463 Commits

Author SHA1 Message Date
a1b0889f49 Merge branch 'master' of https://git.fascinated.cc/Fascinated/scoresaber-reloadedv3
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m59s
2024-10-29 21:51:15 +00:00
a5604335c1 fix a few pages not being the full screen height
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 3m4s
2024-10-29 17:47:20 -04:00
acec87bb17 add device set on fallback
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-29 21:47:02 +00:00
24f4910364 Merge remote-tracking branch 'origin/master'
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 5s
2024-10-29 21:43:45 +00:00
b4095e3bf6 update score feed page 2024-10-29 21:43:10 +00:00
9979732cc6 small responsiveness fix
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-29 17:42:56 -04:00
cd8bbeff5d new footer
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 3m0s
2024-10-29 17:39:32 -04:00
803edb4fd5 update icons
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 3m1s
2024-10-29 16:34:16 -04:00
5774f51b06 make score difficulty random 2024-10-29 16:25:57 -04:00
8814b9881e make realtime scores mobile responsive 2024-10-29 16:16:04 -04:00
e1c665193b redeploy site
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 3m17s
2024-10-29 16:01:08 -04:00
3491057f04 update bun lock 2024-10-29 15:59:58 -04:00
8f6c556662 more landing page stuff (:
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 8s
2024-10-29 15:50:58 -04:00
a47cc3c7d8 disable all time until i can make fetching it faster
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m53s
2024-10-29 19:50:40 +00:00
be5f0ab780 testing
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-29 19:36:11 +00:00
de441b698c smh my head
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 58s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m55s
2024-10-29 18:52:14 +00:00
448155a3c3 smh my head
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 34s
Deploy Website / docker (ubuntu-latest) (push) Failing after 34s
2024-10-29 18:49:20 +00:00
78c8c1ba98 add some optimization
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
2024-10-29 18:48:09 +00:00
069a566d40 add additional data and previous score in top scores response
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
2024-10-29 18:45:34 +00:00
8011ed7b5a add additional data in top scores response
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 31s
2024-10-29 18:44:21 +00:00
f232468fc1 fix grr
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 33s
Deploy Website / docker (ubuntu-latest) (push) Failing after 31s
2024-10-29 18:39:27 +00:00
b68de0552f cleanup top scores and add timeframes to them
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 45s
Deploy Website / docker (ubuntu-latest) (push) Failing after 32s
2024-10-29 18:34:58 +00:00
9e96d2f0ba replace discord log with console log 2024-10-29 14:42:34 +00:00
f4192b5030 remove debug 2024-10-29 14:17:49 +00:00
b610c5d97f fix cron 2024-10-29 14:17:42 +00:00
67c4865697 fix player name 2024-10-29 14:17:33 +00:00
3a2a876f74 fix player scores background refreshing
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 52s
2024-10-29 14:11:30 +00:00
a26bf53996 this is useless
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m52s
2024-10-29 12:39:57 +00:00
57e74e30e2 Merge remote-tracking branch 'origin/master'
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 3m14s
2024-10-28 22:53:13 -04:00
f197d0b3c3 test 2024-10-29 02:50:34 +00:00
a80213aa51 Begin on the new landing page 2024-10-28 22:47:45 -04:00
e9c03a662e add is prod check to Sentry
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m46s
2024-10-28 22:14:00 +00:00
18aaba86be testing sentry stuff
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m49s
2024-10-28 21:53:37 +00:00
72a9cee7af testing sentry stuff
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m3s
2024-10-28 21:44:15 +00:00
314ade1457 testing sentry stuff
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m19s
2024-10-28 21:38:23 +00:00
01cc5d8c48 testing sentry stuff 2024-10-28 21:36:39 +00:00
e5f0bd0595 bundle analyzer
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m37s
2024-10-28 21:31:04 +00:00
3c7cedc529 fix tge fix
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m19s
2024-10-28 21:08:02 +00:00
e88fb50b14 change song author and mapper name sizing
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m32s
2024-10-28 21:05:29 +00:00
6b30c6efed cleanup
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m33s
2024-10-28 21:01:12 +00:00
ebb91344bc fix previous score timestamp hover
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-28 19:10:30 +00:00
02015525e3 oops
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-28 19:01:47 +00:00
c1f33578d7 fix vs for mobile
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-28 16:33:21 +00:00
0ec1fc9d41 oops
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m19s
2024-10-28 16:30:12 +00:00
1c2214a659 made the player page look much nicer
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-28 16:22:33 +00:00
f156b7f582 fix previous score
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-28 15:47:41 +00:00
ad568ddf5d fix player data tracking
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-28 14:00:12 +00:00
df297d0c99 maybe fix player data tracking idk
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-28 13:35:14 +00:00
c8fb08b192 add embeds to top and live scores pages
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-28 13:28:54 +00:00
981bc13a1f update player name
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-28 13:27:46 +00:00
8314cbcf2d bob the builder
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-28 13:22:23 +00:00
f52b62ba83 add top scores page
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m31s
2024-10-28 13:18:40 +00:00
0a5d42f6ac make player score fetching much faster
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-28 12:12:44 +00:00
ce65116db4 log this to see if its why some don't get tracked
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m12s
2024-10-28 11:46:03 +00:00
6c81316364 auto reload site stats
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-27 19:30:58 +00:00
b83fb6f3a8 cache statistics
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m20s
2024-10-27 15:17:54 +00:00
c58f24103f update lgo
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-27 15:13:43 +00:00
e146d20f4f log ss score tracking
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-27 15:11:00 +00:00
ffa4ab2b6c add PP to the acc chart for a score
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-27 15:08:20 +00:00
b889eee7ff change connection idle timeout
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-27 14:03:41 +00:00
28e8561020 cleanup and track player history when creating the player instead of waiting for the cron job
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-27 14:01:12 +00:00
de3768559f include today in seeding player history
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 42s
2024-10-27 13:35:45 +00:00
4be0b072b2 fix player embed image not updating (maybe?)
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-27 13:30:51 +00:00
d086e922c4 fix leaderboard id
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-27 11:20:08 +00:00
96ab9be79a maybe this will help the memory usage idk
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-27 10:59:41 +00:00
3357939071 maybe fix backend crash?
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m13s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-27 10:45:20 +00:00
f3737ce7a5 format previous score
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m20s
2024-10-26 18:44:57 +01:00
9626931b91 migrate some values to ssr data tracking so we don't need to rely on BL as much
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 49s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-26 18:41:51 +01:00
5ff0d11f5a show pause count for scores
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-26 18:02:04 +01:00
5f4d3829e2 no need to log this
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m19s
2024-10-26 15:45:40 +01:00
b3cd770724 move score feed button to the footer
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m18s
2024-10-26 15:38:24 +01:00
c3a75b139a minimize api calls to scoresaber
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-26 15:33:05 +01:00
ba80b9623b revert react
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m40s
2024-10-26 15:19:03 +01:00
e57e725639 optimize requests to scoresaber's api
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m57s
2024-10-26 15:16:50 +01:00
f8b0f7c6cd cleanup + bump next & react
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 2m12s
2024-10-26 14:58:44 +01:00
0d39a905f6 never refresh ranked leaderboards
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-26 13:27:53 +01:00
7be8c37779 move this to the cached leaderboard
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-26 13:22:43 +01:00
6bc2e09f43 meow
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-26 13:16:55 +01:00
da7f5f1c62 fix imports
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 8s
2024-10-26 13:14:25 +01:00
fe888d9fb6 cache scoresaber leaderboards
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-26 13:13:32 +01:00
a8eb2372cb fix imports
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-26 11:46:24 +01:00
e0c719eaba highlight correct player on leaderboard scores and make player name clickable on a leaderboard score
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-26 11:45:34 +01:00
413d72182d add per hand real accuracy (eg: 95%)
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m27s
2024-10-26 11:38:16 +01:00
d7929cc36a fix player daily score set
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m47s
2024-10-26 01:05:51 +01:00
dd162bf77c fix mobile score
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-25 21:56:52 +01:00
5d7bdc17b1 fix score history icon
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 6s
2024-10-25 21:42:21 +01:00
ff287222f7 reset dropdown mode on dropdown close
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-25 21:40:07 +01:00
3abffec9cb oops
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-25 21:34:26 +01:00
f20d83a436 fix error
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 34s
Deploy Website / docker (ubuntu-latest) (push) Failing after 32s
2024-10-25 21:32:59 +01:00
97fba47fd8 add score history viewing
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 34s
Deploy Website / docker (ubuntu-latest) (push) Failing after 33s
2024-10-25 21:29:57 +01:00
9fb5317bc8 maybe it just needs time to work idk
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 43s
2024-10-25 18:29:57 +01:00
7e1d172b43 fix perms
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-25 18:01:44 +01:00
da950e08f2 bot stuff
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
2024-10-25 18:00:15 +01:00
Lee
2b9a777506 Update projects/backend/src/bot/bot.ts
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-25 16:49:38 +00:00
90b0994524 add bot cmd
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 30s
2024-10-25 17:44:19 +01:00
53e0ce007d rename cron 2024-10-25 17:39:25 +01:00
a421243973 ensure scores are always up-to-date for players
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-25 17:37:56 +01:00
b911072a47 change wording
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-25 14:25:04 +01:00
59d5cdb2ae only scroll to on leaderboard page
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-25 13:38:11 +01:00
a9338393f5 add default values for rankedScores and unrankedScores stat
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m12s
2024-10-25 13:37:04 +01:00
6d0c6aa47f add more statistics
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m26s
2024-10-24 14:36:24 +01:00
aaee96ad7b cleanup imports
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-24 14:32:37 +01:00
cd1f010698 fix crying
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-24 14:28:18 +01:00
1d9433ef02 fix score acc slider not updating the acc and add a loader to the leaderboard dropdown
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m33s
2024-10-24 13:30:37 +01:00
9f4e3afffd Merge remote-tracking branch 'origin/master' 2024-10-24 09:11:26 +01:00
4232add9c7 bump lock file 2024-10-24 09:11:21 +01:00
Lee
3b2a42a995 Merge pull request 'Update oven/bun Docker tag to v1.1.33' (#87) from renovate/oven-bun-1.x into master
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 18s
Deploy Website / docker (ubuntu-latest) (push) Failing after 9s
Reviewed-on: #87
2024-10-24 08:08:24 +00:00
02083204f7 Update oven/bun Docker tag to v1.1.33 2024-10-24 08:03:37 +00:00
fc38aba6f4 hide total ranked scores side text on mobile
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m31s
2024-10-23 23:26:40 +01:00
781a3e8cdc fix previous score miss counts
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m32s
2024-10-23 23:18:37 +01:00
20376070c3 show role color on role badge
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m56s
2024-10-23 23:05:20 +01:00
42264ece64 oopsie
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m35s
2024-10-23 20:54:20 +01:00
2852e0c0ed track difficulty and characteristic for scoresaber scores
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
Deploy Website / docker (ubuntu-latest) (push) Failing after 30s
2024-10-23 20:52:57 +01:00
0a87877373 ignore score if it already exists
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
2024-10-23 20:47:12 +01:00
b8f6829f71 literally nothing uses this
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m31s
2024-10-23 20:30:33 +01:00
44bc812ad8 fix
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m35s
2024-10-23 20:26:48 +01:00
d1d12b4193 fix import
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m39s
2024-10-23 20:24:16 +01:00
d42c888e82 implement scoresaber score tracking (for previous scores)
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m6s
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m40s
2024-10-23 20:20:57 +01:00
3c4406c4b7 format obstacles
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-23 17:59:56 +01:00
2a681e6b32 oops
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-23 17:47:54 +01:00
90c57ad086 add score acc chart
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
Deploy Website / docker (ubuntu-latest) (push) Failing after 31s
2024-10-23 17:44:55 +01:00
0731d20edc tooltipsssssssssss
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-23 16:44:08 +01:00
0d12e7c024 fix score feed acc
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-23 16:41:31 +01:00
e403d1f241 this might help a little bit
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-23 16:30:53 +01:00
b4bcf32a43 maybe fix tooltip on mobile?
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 2m22s
2024-10-23 16:23:03 +01:00
56b2f272b9 maybe fix tooltip on mobile?
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-23 16:22:41 +01:00
55b9f0e4ef fix swagger
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-23 16:19:29 +01:00
1bc2b35ec0 lookup beat saver map hash inside the versions not the _id
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 39s
2024-10-23 16:08:26 +01:00
ed4bcc93e1 typescript makes me want to kms sometimes.
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-23 15:52:25 +01:00
de3dec22de it's really not that hard, just build
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m35s
2024-10-23 15:45:56 +01:00
4b5c2acad5 oh?
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m38s
2024-10-23 15:42:38 +01:00
6e38f36945 oops
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 29s
2024-10-23 15:40:47 +01:00
584af8c5a4 upsert the beatsaver map
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Has been cancelled
2024-10-23 15:40:28 +01:00
0f68b2b69e maybe fix?
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 32s
Deploy Website / docker (ubuntu-latest) (push) Failing after 32s
2024-10-23 15:37:21 +01:00
33b931b5f1 add map stats from beat saver
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m34s
2024-10-23 15:33:25 +01:00
62090b8054 i am smart
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-23 09:24:48 +01:00
f8e0326dec oops
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-23 09:06:44 +01:00
c09a50b8a2 add previous days query to player history
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 39s
2024-10-23 09:02:11 +01:00
55cbcb3d66 bump lock file 2024-10-23 08:46:15 +01:00
Lee
fd95f9414f Merge pull request 'Update dependency @sentry/nextjs to v8.35.0' (#79) from renovate/sentry-javascript-monorepo into master
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 11s
Reviewed-on: #79
2024-10-23 07:45:31 +00:00
Lee
05e10424ef Merge pull request 'Update dependency lucide-react to ^0.453.0' (#80) from renovate/lucide-monorepo into master
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
Reviewed-on: #80
2024-10-23 07:45:26 +00:00
Lee
d3ba6eedc4 Merge pull request 'Update oven/bun Docker tag to v1.1.32' (#84) from renovate/oven-bun-1.x into master
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m9s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
Reviewed-on: #84
2024-10-23 07:41:36 +00:00
6bbf628ab5 fix
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-23 08:36:55 +01:00
56ae9b717c fix mini ranking for top players
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-23 08:24:31 +01:00
08295d7b04 make the score improvement text smaller and show a previous pp inaccuracy warning
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-23 08:13:07 +01:00
8090361615 cleanup score badges
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-23 07:52:23 +01:00
299cf20cb9 fix hand acc
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-23 07:40:11 +01:00
ff9ff5b96b cleanup hand acc
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m44s
2024-10-23 07:31:43 +01:00
c3cf48e731 maybe fix tooltips on mobile?
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-22 22:52:56 +01:00
1befe6cc57 add fc pp to pp hover
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m21s
2024-10-22 22:48:01 +01:00
7b008d8e55 fix ranking page links
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m26s
2024-10-22 21:20:13 +01:00
68e343083b fix timestamp
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 39s
2024-10-22 19:10:18 +01:00
989d66780d add timestamp to additional data
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-22 19:08:22 +01:00
ca8fb41fab fix error
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-22 18:55:47 +01:00
6a1b18581f add score
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 44s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-22 18:54:21 +01:00
a33c1b81b7 fix scores set today
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-22 18:46:57 +01:00
6495db7588 meh
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m6s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-22 18:36:48 +01:00
c3ab9851ab fix total ranked score stat
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 9s
Deploy Website / docker (ubuntu-latest) (push) Failing after 12s
2024-10-22 18:34:32 +01:00
ef287d6c3c fix whatever the fuck this bug was
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 9s
2024-10-22 18:33:00 +01:00
cf84ebe456 smh my head
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 8s
Deploy Website / docker (ubuntu-latest) (push) Failing after 7s
2024-10-22 18:29:11 +01:00
220cf31511 smh my head
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 12s
Deploy Website / docker (ubuntu-latest) (push) Failing after 9s
2024-10-22 18:25:26 +01:00
3f63225f16 smh my head 2024-10-22 18:24:46 +01:00
50bc341c38 fix package caching?
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 8s
Deploy Website / docker (ubuntu-latest) (push) Failing after 8s
2024-10-22 18:23:25 +01:00
7b87188e98 add bl replay button
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m25s
2024-10-22 18:20:34 +01:00
75f79e34b7 store bl score and leaderboard id
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-22 18:10:33 +01:00
2fc8b265d2 fix previous score fc tooltip
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m23s
2024-10-22 17:55:05 +01:00
f090c0dcbb bl score fixes
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-22 17:48:32 +01:00
9c20aff89d add bombs to miss count
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m30s
2024-10-22 17:35:14 +01:00
36ab7eb4cf oopsie
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 54s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-22 17:32:09 +01:00
f3dee6a7d2 rework beatleader data tracking
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 34s
Deploy Website / docker (ubuntu-latest) (push) Failing after 32s
2024-10-22 17:30:14 +01:00
fa2ba83c7a add beatleader data tracking!!!!!!!!!!!!!
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 44s
Deploy Website / docker (ubuntu-latest) (push) Failing after 38s
2024-10-22 15:59:41 +01:00
074d4de123 fix imports
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m24s
2024-10-22 13:55:58 +01:00
854f88c43a track total score, total ranked score, replay watched count and add a score chart
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 31s
Deploy Website / docker (ubuntu-latest) (push) Failing after 30s
2024-10-22 13:54:54 +01:00
15e6cb85c4 Update dependency lucide-react to ^0.453.0 2024-10-22 12:02:52 +00:00
696da236d5 bump nextjs
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m7s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m8s
2024-10-22 12:37:52 +01:00
f89207f306 fix total score change
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-22 12:34:17 +01:00
be25896c5e change some color stuff
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m34s
2024-10-22 12:26:58 +01:00
fbf5603866 Update oven/bun Docker tag to v1.1.32 2024-10-21 21:03:02 +00:00
d62b6524f7 cleanup
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m10s
2024-10-21 17:39:47 +01:00
de47905e28 make leaderboard info column slightly bigger
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m10s
2024-10-21 15:35:26 +01:00
9a621eea82 oh?
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m12s
2024-10-21 14:21:11 +01:00
42e0c3a7b2 still include today
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 44s
Deploy Website / docker (ubuntu-latest) (push) Failing after 59s
2024-10-21 14:18:26 +01:00
0b92cec911 fix graph only showing 49 days of data
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Failing after 57s
2024-10-21 14:15:59 +01:00
5933074569 fix qualified status for leaderboards
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Failing after 59s
2024-10-21 13:47:53 +01:00
af8c87f5af add player role badge
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Failing after 57s
2024-10-21 13:39:02 +01:00
173703664d oops
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m5s
2024-10-21 13:29:23 +01:00
077bb6d73b add acc slider to the pp graph
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-21 13:27:47 +01:00
78e3ec43d7 Update dependency @sentry/nextjs to v8.35.0 2024-10-21 12:02:38 +00:00
b7349f0226 fix scores history being reset for that day
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
2024-10-21 07:45:23 +01:00
ad826d7a3f make all queries re fetch every 5 mins
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m12s
2024-10-21 07:43:18 +01:00
6baeab930d add more info to leaderboard score misses
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-21 07:33:46 +01:00
87b2c7c48a fix player header pp color
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m8s
2024-10-21 07:06:42 +01:00
b9587feb9e re-add days ago to the chart
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-21 07:04:39 +01:00
fad22274fd make song name clickable on leaderboard page (goes to beatsaver map)
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-21 06:56:20 +01:00
577fcb0e0d Merge remote-tracking branch 'origin/master'
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m31s
2024-10-21 06:47:58 +01:00
c3d4d1fe1f add leaderboard pp chart 2024-10-21 06:47:53 +01:00
Lee
a15893ea56 Update projects/website/src/components/player/player-scores.tsx
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-20 22:10:02 +00:00
Lee
d0bcd29796 Delete projects/website/src/middleware.ts
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m8s
2024-10-20 22:03:38 +00:00
81640c3c4e fix
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 44s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m43s
2024-10-20 19:52:33 +01:00
06a13bedc8 use new stat for ranked and total play count change
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 31s
Deploy Website / docker (ubuntu-latest) (push) Failing after 33s
2024-10-20 19:49:10 +01:00
bded9969fe track total scores and total ranked scores
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 44s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m13s
2024-10-20 19:34:54 +01:00
cd2f8c0925 pass through the leaderboard
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m12s
2024-10-20 19:25:32 +01:00
d1a9654e33 hide leaderboard pp on unranked maps
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 42s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-20 19:24:31 +01:00
336518ff70 cleanup stat changes and add ranked and total scores change
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m13s
2024-10-20 19:17:17 +01:00
a68e53734d add default stale time to queries
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-20 14:04:55 +01:00
9d2a26fa07 skip 999_999 on fallback data
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m10s
2024-10-20 14:01:47 +01:00
a0dd1b4601 fix country flag sizing
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-20 13:59:48 +01:00
fcb84f820b fix imports
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m10s
2024-10-20 13:53:30 +01:00
d806907604 update score badge tooltips
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m2s
2024-10-20 13:49:14 +01:00
511f56af91 add a "-" for no modifiers
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 1m0s
2024-10-20 13:44:36 +01:00
57a9780fe8 make mapper name clickable on the leaderboard
Some checks are pending
Deploy Website / docker (ubuntu-latest) (push) Waiting to run
Deploy Backend / docker (ubuntu-latest) (push) Successful in 1m11s
2024-10-20 13:43:25 +01:00
4d8debe333 fix easy color
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m12s
2024-10-19 18:17:43 +01:00
899c3e11e6 remove replay stuff
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-19 17:51:24 +01:00
8f617aca82 fix mini hover on player
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-19 17:48:29 +01:00
9b549f8dc6 cleanup
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m15s
2024-10-19 17:45:02 +01:00
1e8c38eb26 fix the url randomly switching to a leaderboard
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m10s
2024-10-19 17:33:47 +01:00
2df95d140a fix tooltips
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m5s
2024-10-19 17:29:17 +01:00
337331538a fix modifiers on score
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m8s
2024-10-19 17:13:55 +01:00
d3ce922f00 cleanup
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-19 17:08:00 +01:00
982202f813 make mini ranking text smaller
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-19 15:37:25 +01:00
a1148d0f59 cleanup
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 6s
2024-10-19 15:31:02 +01:00
0d182d3ff4 update mini ranking
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled
2024-10-19 15:28:32 +01:00
7465f854e0 fix around me for top players
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-19 15:21:57 +01:00
670f2047a0 show all modifiers now
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-19 15:12:06 +01:00
d2be3d833b add role colors
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-19 14:41:01 +01:00
16c34adc19 redesign leaderboard scores
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m9s
2024-10-19 14:11:43 +01:00
caf5f01a09 make diff buttons look nicer
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m7s
2024-10-19 13:13:36 +01:00
e0aeec5d5a log rate limit left
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 47s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-19 12:48:57 +01:00
79bdb801ff add no fail to acc
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m12s
2024-10-19 12:33:26 +01:00
cec3541345 refresh interval on scores
All checks were successful
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m11s
2024-10-19 11:31:53 +01:00
ac6aaee208 fix #1 log
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 40s
2024-10-19 11:17:08 +01:00
a773488e9b fetch spamming ss api
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
2024-10-19 09:58:30 +01:00
1f4be74c54 testing
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 48s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m22s
2024-10-19 09:36:06 +01:00
31f57cbe6b testing
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 1m9s
2024-10-19 09:19:43 +01:00
de05aceb9f fix these showing when they shouldn't
All checks were successful
Deploy Website / deploy (push) Successful in 5m35s
2024-10-19 09:07:01 +01:00
4eb96da1f1 add more info to offline network check
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Deploy Website / deploy (push) Successful in 5m32s
2024-10-19 07:52:19 +01:00
0931e52df5 fix score ui issue when on mobile and add swagger to the footer
Some checks failed
Deploy Backend / deploy (push) Failing after 2m25s
Deploy Website / deploy (push) Successful in 7m19s
2024-10-19 07:36:20 +01:00
37b491a0b5 fix swagger?
All checks were successful
Deploy Backend / deploy (push) Successful in 4m5s
2024-10-19 07:15:28 +01:00
a8c40f50d6 fix swagger?
All checks were successful
Deploy Backend / deploy (push) Successful in 3m23s
2024-10-19 06:56:07 +01:00
e1f5a13f57 don't cache this
All checks were successful
Deploy Backend / deploy (push) Successful in 5m51s
2024-10-19 04:54:24 +01:00
c5bfdc8b9c migrate around me to the backend
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Deploy Website / deploy (push) Successful in 5m53s
2024-10-19 04:53:17 +01:00
c40b8b5d8e migrate around me to the backend 2024-10-19 04:53:06 +01:00
7421c47959 cleanup caching
All checks were successful
Deploy Backend / deploy (push) Successful in 4m0s
Deploy Website / deploy (push) Successful in 7m9s
2024-10-19 04:10:44 +01:00
238ec6e254 don't proxy requests that happen on the server
All checks were successful
Deploy Backend / deploy (push) Successful in 3m36s
Deploy Website / deploy (push) Successful in 6m8s
2024-10-19 03:50:29 +01:00
Lee
a6576e9730 Merge pull request 'Update oven/bun Docker tag to v1.1.31' (#81) from renovate/oven-bun-1.x into master
All checks were successful
Deploy Backend / deploy (push) Successful in 3m26s
Deploy Website / deploy (push) Successful in 5m34s
Reviewed-on: #81
2024-10-18 11:11:32 +00:00
60ac8d17c5 update #1 log embed
All checks were successful
Deploy Backend / deploy (push) Successful in 4m9s
2024-10-18 10:33:11 +01:00
8713ee3e02 update #1 log embed
Some checks are pending
Deploy Backend / deploy (push) Waiting to run
2024-10-18 10:32:13 +01:00
3a734075e0 add steam profile button and fix score acc
All checks were successful
Deploy Backend / deploy (push) Successful in 3m18s
Deploy Website / deploy (push) Successful in 5m43s
2024-10-18 10:27:38 +01:00
6c8ef89bb5 remove the rate limiter
All checks were successful
Deploy Backend / deploy (push) Successful in 3m35s
2024-10-18 08:22:54 +01:00
0317eae926 fix beatsaver map causing page to not load if it's unknown
All checks were successful
Deploy Backend / deploy (push) Successful in 3m46s
Deploy Website / deploy (push) Successful in 4m46s
2024-10-18 08:13:26 +01:00
a636e7aa08 oops
All checks were successful
Deploy Backend / deploy (push) Successful in 3m48s
Deploy Website / deploy (push) Successful in 4m32s
2024-10-18 07:59:41 +01:00
9fb276ec4e show more data on the #1 feed
Some checks failed
Deploy Backend / deploy (push) Failing after 1m26s
Deploy Website / deploy (push) Failing after 1m5s
2024-10-18 07:56:39 +01:00
4a966344f2 Update oven/bun Docker tag to v1.1.31 2024-10-18 05:02:48 +00:00
dd8befa9e0 fix logs
All checks were successful
Deploy Backend / deploy (push) Successful in 4m17s
Deploy Website / deploy (push) Successful in 5m36s
2024-10-17 19:02:32 +01:00
1350cdc0b1 fix imports
All checks were successful
Deploy Backend / deploy (push) Successful in 3m48s
2024-10-17 18:37:47 +01:00
c43f27a6ac fix bot secret
Some checks failed
Deploy Backend / deploy (push) Failing after 1m36s
Deploy Website / deploy (push) Successful in 5m7s
2024-10-17 18:32:11 +01:00
a086bebc40 a super shitty discord bot but i needed logging
Some checks failed
Deploy Backend / deploy (push) Failing after 1m29s
Deploy Website / deploy (push) Has been cancelled
2024-10-17 18:29:30 +01:00
373a6355a6 make ws more reliable
All checks were successful
Deploy Backend / deploy (push) Successful in 3m29s
Deploy Website / deploy (push) Successful in 5m19s
2024-10-17 18:01:34 +01:00
0614b52745 send a heartbeat to the scoresaber ws
All checks were successful
Deploy Backend / deploy (push) Successful in 3m8s
Deploy Website / deploy (push) Successful in 4m55s
2024-10-17 17:46:57 +01:00
c72230a98d cache unknown beatsaver maps
All checks were successful
Deploy Backend / deploy (push) Successful in 2m53s
Deploy Website / deploy (push) Successful in 4m46s
2024-10-17 16:24:10 +01:00
73b7d17597 surely it works now
All checks were successful
Deploy Website / deploy (push) Successful in 3m55s
2024-10-17 15:57:51 +01:00
ba0a406eb4 7?
Some checks failed
Deploy Website / deploy (push) Failing after 1m41s
2024-10-17 15:53:12 +01:00
ccf229ade4 7?
Some checks failed
Deploy Website / deploy (push) Failing after 2m23s
2024-10-17 15:49:28 +01:00
d08f81b25d 7?
Some checks failed
Deploy Website / deploy (push) Failing after 1m36s
Deploy Backend / deploy (push) Successful in 3m11s
2024-10-17 15:47:20 +01:00
e37f0d5548 7 2024-10-17 15:37:13 +01:00
0231c6ccfe oops
Some checks failed
Deploy Website / deploy (push) Failing after 1m38s
2024-10-17 15:35:15 +01:00
b3c124631a move score page fetching to the backend
Some checks failed
Deploy Backend / deploy (push) Successful in 2m26s
Deploy Website / deploy (push) Failing after 1m52s
2024-10-17 15:30:14 +01:00
118dc9d9f1 Fix missing rank data on partial tracked days
All checks were successful
Deploy Backend / deploy (push) Successful in 3m2s
Deploy Website / deploy (push) Successful in 4m41s
2024-10-17 07:15:12 +01:00
7f5587546c cleanup and track friends data (if not being already tracked)
Some checks failed
Deploy Backend / deploy (push) Successful in 2m48s
Deploy Website / deploy (push) Has been cancelled
2024-10-17 07:12:03 +01:00
64f918c325 fix env vars 2024-10-17 06:53:31 +01:00
82b0a0ee71 don't show stat change if player is inactive or banned
All checks were successful
Deploy Website / deploy (push) Successful in 4m52s
2024-10-17 03:23:34 +01:00
b8553c3138 fix building
All checks were successful
Deploy Backend / deploy (push) Successful in 3m32s
Deploy Website / deploy (push) Successful in 4m58s
2024-10-17 03:17:42 +01:00
f83492ffdc show badges for inactive players 2024-10-17 03:13:37 +01:00
b5cfbf384a stupid
Some checks failed
Deploy Backend / deploy (push) Failing after 1m28s
Deploy Website / deploy (push) Failing after 1m35s
2024-10-17 03:11:28 +01:00
c64f046df3 fix score leaderboard staying open when switching sort/page
Some checks failed
Deploy Backend / deploy (push) Failing after 1m27s
Deploy Website / deploy (push) Failing after 1m32s
2024-10-17 03:08:27 +01:00
42d133bbbb fix player data not showing when claiming the profile
Some checks failed
Deploy Backend / deploy (push) Failing after 1m5s
Deploy Website / deploy (push) Failing after 1m29s
2024-10-17 02:40:18 +01:00
ae4e6912e5 add joined date stat
All checks were successful
Deploy Backend / deploy (push) Successful in 2m52s
Deploy Website / deploy (push) Successful in 4m36s
2024-10-17 02:21:59 +01:00
5263509bac add status page to the footer
All checks were successful
Deploy Backend / deploy (push) Successful in 4m12s
Deploy Website / deploy (push) Successful in 7m6s
2024-10-16 11:40:46 +01:00
24a15f97d1 fix issue with stat tracking
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-16 11:36:06 +01:00
2367a03516 make the navbar move around less
All checks were successful
Deploy Website / deploy (push) Successful in 4m59s
2024-10-16 11:33:24 +01:00
b5ae8a8ae0 always enable logging
All checks were successful
Deploy Backend / deploy (push) Successful in 3m39s
2024-10-16 11:27:04 +01:00
8ab81b1b27 impl friends system
All checks were successful
Deploy Backend / deploy (push) Successful in 2m43s
Deploy Website / deploy (push) Successful in 4m55s
2024-10-16 11:23:28 +01:00
2e2c03241e oops
All checks were successful
Deploy Website / deploy (push) Successful in 4m48s
2024-10-16 08:30:34 +01:00
1e8a9b9a59 fix theme color
Some checks failed
Deploy Backend / deploy (push) Successful in 4m25s
Deploy Website / deploy (push) Failing after 2m45s
2024-10-16 08:21:27 +01:00
cb7143ed3d should be all good now (and added api status notifications)
Some checks failed
Deploy Backend / deploy (push) Successful in 3m38s
Deploy Website / deploy (push) Has been cancelled
2024-10-16 08:15:11 +01:00
1eed0e1e99 im dumb 2024-10-16 08:03:36 +01:00
6d6e59ed13 oops
Some checks failed
Deploy Backend / deploy (push) Failing after 2m23s
2024-10-16 07:50:25 +01:00
3dcf03ce53 imports
Some checks failed
Deploy Backend / deploy (push) Failing after 1m35s
Deploy Website / deploy (push) Successful in 5m38s
2024-10-16 07:48:20 +01:00
045f605cc6 switch lib 2024-10-16 07:47:52 +01:00
ff9408fb8c re-add per page/leaderboard embed colors
All checks were successful
Deploy Backend / deploy (push) Successful in 3m31s
Deploy Website / deploy (push) Successful in 5m17s
2024-10-16 07:31:52 +01:00
7f42a27d8f add new player tracking logging and add discord link to the footer
All checks were successful
Deploy Backend / deploy (push) Successful in 3m5s
Deploy Website / deploy (push) Successful in 5m44s
2024-10-16 06:53:30 +01:00
ed21d3d780 add twitter to the footer
All checks were successful
Deploy Website / deploy (push) Successful in 4m24s
2024-10-16 06:36:24 +01:00
7bacc30f33 cleanup images and add daily change and join date to the player image
All checks were successful
Deploy Backend / deploy (push) Successful in 2m35s
2024-10-16 06:16:55 +01:00
74385252a4 how did this even get removed
All checks were successful
Deploy Backend / deploy (push) Successful in 3m38s
2024-10-16 03:08:54 +01:00
6ee4c5b754 silly lock file 2024-10-16 03:04:15 +01:00
78c88acddf there
Some checks failed
Deploy Backend / deploy (push) Failing after 31s
2024-10-16 03:03:15 +01:00
ee1c33bcc9 oops
All checks were successful
Deploy Backend / deploy (push) Successful in 3m53s
Deploy Website / deploy (push) Successful in 5m26s
2024-10-16 02:39:42 +01:00
013d866391 now!
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-16 02:35:17 +01:00
9355f53ee5 add og image caching
All checks were successful
Deploy Backend / deploy (push) Successful in 4m11s
2024-10-16 02:31:10 +01:00
a5e00e4850 add leaderboard embed image
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Deploy Website / deploy (push) Failing after 2m19s
2024-10-16 02:27:59 +01:00
3b691dae3c update player og image
All checks were successful
Deploy Backend / deploy (push) Successful in 2m55s
2024-10-15 23:22:29 +01:00
5998eac6f7 add player page embed desc
All checks were successful
Deploy Website / deploy (push) Successful in 4m18s
2024-10-15 23:02:30 +01:00
22abdab10b it buildssssssssssssss
All checks were successful
Deploy Website / deploy (push) Successful in 4m33s
2024-10-15 20:32:58 +01:00
da7345b929 7?
Some checks failed
Deploy Website / deploy (push) Failing after 2m42s
2024-10-15 20:11:22 +01:00
eb1d2899b9 please just build
Some checks failed
Deploy Website / deploy (push) Failing after 2m44s
2024-10-15 20:06:18 +01:00
a8af4a9d45 maybe now
Some checks failed
Deploy Website / deploy (push) Failing after 3m15s
2024-10-15 20:02:10 +01:00
2af45b1508 build now pls
Some checks failed
Deploy Website / deploy (push) Failing after 2m41s
2024-10-15 19:56:44 +01:00
f2ef170f01 fix
Some checks failed
Deploy Backend / deploy (push) Successful in 4m14s
Deploy Website / deploy (push) Failing after 2m42s
2024-10-15 19:43:23 +01:00
970ab22e2f cleanup
Some checks failed
Deploy Backend / deploy (push) Successful in 4m37s
Deploy Website / deploy (push) Failing after 3m23s
2024-10-15 19:32:06 +01:00
d56a85c342 cleanup 2024-10-15 19:31:50 +01:00
f303794f5c cleanup
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-15 19:26:55 +01:00
6f88ab8f30 move og image to backend
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Deploy Website / deploy (push) Failing after 2m39s
2024-10-15 19:26:04 +01:00
ef634194b8 add cool og image for player embed
All checks were successful
Deploy Backend / deploy (push) Successful in 3m39s
Deploy Website / deploy (push) Successful in 6m47s
2024-10-15 18:59:13 +01:00
005e05d8fb ples work now xoxo
All checks were successful
Deploy Website / deploy (push) Successful in 5m48s
2024-10-15 04:21:53 +01:00
b803362360 fix this 2024-10-15 04:16:24 +01:00
80c1c95014 add a ":" to the chart tooltip to split name and value
All checks were successful
Deploy Website / deploy (push) Successful in 5m31s
2024-10-15 04:11:48 +01:00
9d38e095fe add daily scores set tracking
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 3m51s
2024-10-15 04:09:47 +01:00
5b3218c205 fix acc chart
All checks were successful
Deploy Website / deploy (push) Successful in 4m30s
2024-10-14 20:37:41 +01:00
8133d18ca2 format weekly rank change 2024-10-14 20:37:14 +01:00
383f41f9ca surely this works
All checks were successful
Deploy Backend / deploy (push) Successful in 4m23s
Deploy Website / deploy (push) Successful in 7m25s
2024-10-14 11:29:00 +01:00
5871b82f75 why no work
All checks were successful
Deploy Website / deploy (push) Successful in 6m47s
2024-10-14 11:19:55 +01:00
52e3ac9cec testing custom error page
All checks were successful
Deploy Website / deploy (push) Successful in 6m38s
2024-10-14 11:11:13 +01:00
055e0869b8 oops
All checks were successful
Deploy Website / deploy (push) Successful in 5m43s
2024-10-14 11:04:58 +01:00
04e0898b3c oops there
All checks were successful
Deploy Website / deploy (push) Successful in 5m51s
2024-10-14 10:58:30 +01:00
a6b99219e1 make the mobile pagination look nicer
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-14 10:55:55 +01:00
eb06801026 make the mobile pagination more useful
All checks were successful
Deploy Website / deploy (push) Successful in 4m33s
2024-10-14 10:45:43 +01:00
ac4298c765 fix pp value on chart?
All checks were successful
Deploy Backend / deploy (push) Successful in 4m27s
Deploy Website / deploy (push) Successful in 6m26s
2024-10-14 03:06:22 +01:00
a15f8f46f9 make the charts nicer
All checks were successful
Deploy Website / deploy (push) Successful in 4m30s
2024-10-14 02:32:32 +01:00
cdf9942924 fix playerchart
All checks were successful
Deploy Website / deploy (push) Successful in 5m6s
2024-10-14 01:43:00 +01:00
67c1775edb remove this log
All checks were successful
Deploy Backend / deploy (push) Successful in 4m39s
Deploy Website / deploy (push) Successful in 9m13s
2024-10-13 05:27:27 +01:00
4d27fe9bae fix score feed getting stuck
All checks were successful
Deploy Website / deploy (push) Successful in 8m21s
2024-10-13 05:14:03 +01:00
aa4ef05b55 fix time formatter
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 5m48s
2024-10-13 05:11:09 +01:00
0f282fd003 use a better hook
All checks were successful
Deploy Website / deploy (push) Successful in 8m1s
2024-10-13 04:58:57 +01:00
c4b5bace5d oops
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-13 04:53:20 +01:00
0e3b2252a5 update "just now" time
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 4m25s
2024-10-13 04:51:16 +01:00
684ac4660e cleanup
All checks were successful
Deploy Website / deploy (push) Successful in 6m5s
2024-10-13 04:41:11 +01:00
4cc5893757 add simple live score feed page
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 4m15s
2024-10-13 04:40:04 +01:00
ee212150fd add default score sort (last selected)
All checks were successful
Deploy Website / deploy (push) Successful in 5m1s
2024-10-13 03:49:33 +01:00
2a61ed26a6 next
All checks were successful
Deploy Website / deploy (push) Successful in 5m40s
2024-10-13 01:54:11 +01:00
bc64e6ef3f next
Some checks failed
Deploy Website / deploy (push) Failing after 2m33s
2024-10-13 01:38:59 +01:00
0b8e693f80 next
Some checks failed
Deploy Website / deploy (push) Failing after 2m36s
2024-10-13 01:35:18 +01:00
17193fe18a remove this
Some checks failed
Deploy Website / deploy (push) Failing after 2m35s
2024-10-13 01:31:40 +01:00
97d7ab2d0a cookies are async now?
Some checks failed
Deploy Website / deploy (push) Failing after 2m33s
2024-10-13 01:28:49 +01:00
81fe9c3bb6 bump sentry
Some checks failed
Deploy Website / deploy (push) Failing after 2m26s
2024-10-13 01:24:28 +01:00
8c3ca26c9c bump react and nextjs
Some checks failed
Deploy Backend / deploy (push) Successful in 5m19s
Deploy Website / deploy (push) Failing after 2m59s
2024-10-13 01:14:51 +01:00
ae2f30a97a add hover to all country flags
All checks were successful
Deploy Website / deploy (push) Successful in 5m5s
2024-10-13 00:41:39 +01:00
b7783f5a4d fix mapper link for song to
All checks were successful
Deploy Website / deploy (push) Successful in 5m3s
2024-10-13 00:37:16 +01:00
783da27b1e make the song name link less fat
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-13 00:34:00 +01:00
1998049509 fix broken chart
All checks were successful
Deploy Website / deploy (push) Successful in 5m10s
2024-10-13 00:20:46 +01:00
ba8579d60c fix ranking page
All checks were successful
Deploy Website / deploy (push) Successful in 3m55s
2024-10-12 22:56:29 +01:00
6d1c911c9f fix rank graph
All checks were successful
Deploy Backend / deploy (push) Successful in 3m8s
Deploy Website / deploy (push) Successful in 3m54s
2024-10-12 22:07:32 +01:00
e2d9a23974 cleanup
All checks were successful
Deploy Website / deploy (push) Successful in 4m55s
2024-10-12 20:29:22 +01:00
b86fb3a609 fixed height for these
All checks were successful
Deploy Website / deploy (push) Successful in 3m55s
2024-10-12 20:24:40 +01:00
cb9bc2143c fix leaderboard pp color
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-12 20:22:08 +01:00
5cc3cca2d7 increase count up duration
All checks were successful
Deploy Website / deploy (push) Successful in 3m35s
2024-10-12 20:18:14 +01:00
3f2dd7ea90 fix player pp display and hide mini rankings on inactive/banned players
All checks were successful
Deploy Website / deploy (push) Successful in 3m22s
2024-10-12 15:38:34 +01:00
5e3ab8435b fix ranking page button on player profile
All checks were successful
Deploy Website / deploy (push) Successful in 3m41s
2024-10-12 15:21:48 +01:00
1917a55725 fix acc/pp being broken on new pages
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-12 15:19:15 +01:00
20a0208e92 fix button gaps
All checks were successful
Deploy Website / deploy (push) Successful in 4m19s
2024-10-12 07:32:41 +01:00
988d8cb17e oops
All checks were successful
Deploy Website / deploy (push) Successful in 4m3s
2024-10-12 07:26:52 +01:00
c73f5c6373 fix mini ranking showing error msg when loading
Some checks failed
Deploy Website / deploy (push) Failing after 1m42s
2024-10-12 07:23:30 +01:00
98e8273c07 add score "edit" mode
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 2m32s
2024-10-12 07:21:55 +01:00
f26b997fbb potential fix for this?
All checks were successful
Deploy Backend / deploy (push) Successful in 2m55s
Deploy Website / deploy (push) Successful in 5m13s
2024-10-12 05:26:30 +01:00
e67fcf328e update claim profile tooltip
All checks were successful
Deploy Website / deploy (push) Successful in 4m9s
2024-10-12 04:40:20 +01:00
eb89987614 fix pp color on ranking page
All checks were successful
Deploy Website / deploy (push) Successful in 4m5s
2024-10-12 04:25:00 +01:00
27c88cdb75 add a basic landing page
All checks were successful
Deploy Backend / deploy (push) Successful in 3m4s
Deploy Website / deploy (push) Successful in 4m30s
2024-10-12 04:12:35 +01:00
0ac70f4781 fix country flag size being inconsistent
All checks were successful
Deploy Website / deploy (push) Successful in 4m6s
2024-10-12 03:48:34 +01:00
fd03e3d6c2 oopsie doodle
Some checks failed
Deploy Backend / deploy (push) Successful in 3m17s
Deploy Website / deploy (push) Has been cancelled
2024-10-12 03:42:43 +01:00
f8b97e3471 cleanup
Some checks failed
Deploy Backend / deploy (push) Failing after 2m2s
Deploy Website / deploy (push) Failing after 1m56s
2024-10-12 03:37:54 +01:00
97a91d7249 fix leaderboard page info not updating
All checks were successful
Deploy Website / deploy (push) Successful in 8m21s
2024-10-12 03:17:39 +01:00
7327b8d169 show no data if data is missing instead of showing 0
Some checks failed
Deploy Backend / deploy (push) Successful in 5m1s
Deploy Website / deploy (push) Has been cancelled
2024-10-12 03:11:02 +01:00
1d6647b74e update wording of change tooltip
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-12 03:06:37 +01:00
8f62c6c694 cleanup
Some checks failed
Deploy Backend / deploy (push) Successful in 5m56s
Deploy Website / deploy (push) Has been cancelled
2024-10-12 02:55:42 +01:00
afdbe0a3dc fix timeframes
All checks were successful
Deploy Backend / deploy (push) Successful in 3m3s
Deploy Website / deploy (push) Successful in 4m15s
2024-10-12 02:41:24 +01:00
2ab3d6b023 track top 1000 players
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-12 02:37:08 +01:00
6947c30c23 fix double fetching of players
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-12 02:36:44 +01:00
786bc69cf3 fix timframe changes showing 0 for some things
All checks were successful
Deploy Backend / deploy (push) Successful in 3m3s
Deploy Website / deploy (push) Successful in 4m54s
2024-10-11 21:45:50 +01:00
6ae69c2fec don't log unless needed
All checks were successful
Deploy Backend / deploy (push) Successful in 2m20s
Deploy Website / deploy (push) Successful in 3m27s
2024-10-11 21:38:30 +01:00
6cd141544c make acc chart 3 places
All checks were successful
Deploy Website / deploy (push) Successful in 5m19s
2024-10-11 20:20:06 +01:00
306269f1f9 cleanup
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-11 20:16:38 +01:00
9f6a58e325 make the star count/diff label smaller
All checks were successful
Deploy Website / deploy (push) Successful in 4m3s
2024-10-11 20:01:49 +01:00
d99feecc8f show more decimal places on the ranked acc chart
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-11 20:00:30 +01:00
ad87365a66 cleanup
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-11 19:58:49 +01:00
d7a3b734ec fix pp
Some checks failed
Deploy Backend / deploy (push) Failing after 1m19s
Deploy Website / deploy (push) Successful in 3m36s
2024-10-11 19:46:03 +01:00
ccedfa2645 add daily, weekly and monthly change to rank, countryRank and pp as a hover
All checks were successful
Deploy Backend / deploy (push) Successful in 2m26s
Deploy Website / deploy (push) Successful in 3m34s
2024-10-11 19:35:39 +01:00
a0681d7b1c add tracking cooldown
All checks were successful
Deploy Backend / deploy (push) Successful in 2m16s
2024-10-11 18:48:04 +01:00
d16cf9e4af add tracking cooldown
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Deploy Website / deploy (push) Successful in 3m18s
2024-10-11 18:47:33 +01:00
29f9b305e7 track top 10 pages, untested but it should work
All checks were successful
Deploy Backend / deploy (push) Successful in 2m27s
2024-10-11 18:42:46 +01:00
dad8afe282 fill in missing data from the scoresaber api data
All checks were successful
Deploy Backend / deploy (push) Successful in 3m0s
Deploy Website / deploy (push) Successful in 4m8s
2024-10-11 04:02:21 +01:00
570f5e1eab fix hover on rank and country rank making the tooltip dim
All checks were successful
Deploy Website / deploy (push) Successful in 4m57s
2024-10-11 03:51:38 +01:00
b059ee3537 always ensure the frontpage of players is being tracked
All checks were successful
Deploy Backend / deploy (push) Successful in 3m19s
2024-10-11 03:47:54 +01:00
0e4feb4181 fix clicking global rank taking you to the wrong page and update url when clicking a player on the ranking page
All checks were successful
Deploy Website / deploy (push) Successful in 3m30s
2024-10-11 03:42:50 +01:00
544e850540 fix clicking global rank taking you to the wrong page and update url when clicking a player on the ranking page
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-11 03:42:42 +01:00
4894e0597c implement a janky fix for mobile until i can be bothered to make it mobile friendly
All checks were successful
Deploy Website / deploy (push) Successful in 3m26s
2024-10-11 03:35:12 +01:00
e4f0376af3 add weekly rank change and change the design a bit
All checks were successful
Deploy Website / deploy (push) Successful in 3m20s
2024-10-11 03:31:52 +01:00
e35c1c77d3 some bug fixes and add the ranking page
All checks were successful
Deploy Backend / deploy (push) Successful in 2m22s
Deploy Website / deploy (push) Successful in 4m8s
2024-10-11 02:43:28 +01:00
f649fb9c7f cleanup getting the player
All checks were successful
Deploy Backend / deploy (push) Successful in 2m12s
2024-10-11 01:12:27 +01:00
26e34c32f1 fix missing acc tracking
All checks were successful
Deploy Backend / deploy (push) Successful in 2m11s
2024-10-11 01:06:02 +01:00
e89ff73b76 fix cron time oops
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-11 01:04:27 +01:00
6b2f9fa308 update log
All checks were successful
Deploy Website / deploy (push) Successful in 3m50s
2024-10-10 02:50:43 +01:00
130016957d oops
All checks were successful
Deploy Website / deploy (push) Successful in 4m39s
2024-10-10 02:34:48 +01:00
19ab2a2e3d log connecting ip
All checks were successful
Deploy Website / deploy (push) Successful in 4m16s
2024-10-10 02:25:54 +01:00
526167d4f1 fix mini rankings
All checks were successful
Deploy Website / deploy (push) Successful in 4m2s
2024-10-10 01:52:05 +01:00
f75897007c oops fix cron
All checks were successful
Deploy Backend / deploy (push) Successful in 3m36s
2024-10-10 01:44:09 +01:00
fc287be481 fix player statistic tracking
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-10 01:40:21 +01:00
0f1c101acc cronjob stuff
All checks were successful
Deploy Backend / deploy (push) Successful in 3m52s
2024-10-10 01:22:09 +01:00
bca3732f1c remove the logs for this - it spams alot 2024-10-10 01:17:55 +01:00
d5cc35da05 force track players bc it never ran
All checks were successful
Deploy Backend / deploy (push) Successful in 3m2s
2024-10-10 01:15:16 +01:00
82116a7405 stupid edge runtime
All checks were successful
Deploy Website / deploy (push) Successful in 4m51s
2024-10-10 00:43:51 +01:00
ee45e41d6d update the logger
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-10 00:39:58 +01:00
886ed4b20c simple logger
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-10 00:36:42 +01:00
ec40f1b564 oops
All checks were successful
Deploy Website / deploy (push) Successful in 4m31s
2024-10-10 00:03:03 +01:00
6443bac879 fix mobile issue on player header
All checks were successful
Deploy Website / deploy (push) Successful in 3m34s
2024-10-09 23:41:59 +01:00
6440001839 fix navbar
All checks were successful
Deploy Website / deploy (push) Successful in 3m55s
2024-10-09 18:15:48 +01:00
a69cf8a033 revert react bump
All checks were successful
Deploy Website / deploy (push) Successful in 4m13s
2024-10-09 18:08:46 +01:00
0293e50cee e 2024-10-09 18:05:51 +01:00
963f62d6a6 Merge remote-tracking branch 'origin/master'
Some checks failed
Deploy Website / deploy (push) Failing after 2m24s
2024-10-09 18:02:52 +01:00
392f7c0db8 disable cache for now 2024-10-09 18:01:51 +01:00
Bun Fixer
179dee0702 Fix bun lock file 2024-10-09 16:56:43 +00:00
2ebc04243c switch to a different cache with a ttl
Some checks failed
Fix bun lock file / build (push) Successful in 41s
Deploy Website / deploy (push) Failing after 2m28s
2024-10-09 17:56:17 +01:00
ee77a8f626 bump react
Some checks failed
Fix bun lock file / build (push) Failing after 1m25s
Deploy Website / deploy (push) Failing after 2m50s
2024-10-09 17:43:15 +01:00
be28191005 make navbar align with the page contents
All checks were successful
Deploy Website / deploy (push) Successful in 4m51s
2024-10-09 17:36:33 +01:00
5be22493fa remove db loaded toast
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-09 17:32:58 +01:00
598b6881e8 Merge remote-tracking branch 'origin/master'
Some checks failed
Fix bun lock file / build (push) Failing after 28s
Deploy Backend / deploy (push) Successful in 2m59s
Deploy Website / deploy (push) Has been cancelled
2024-10-09 17:28:41 +01:00
1faf896c08 this might help with the memory issues..?? 2024-10-09 17:27:37 +01:00
d0e4e1553b maybe this will run first with this? idk 2024-10-09 15:35:14 +01:00
Bun Fixer
10c41d820e Fix bun lock file 2024-10-09 14:34:11 +00:00
7eac8569f3 add manual 2024-10-09 15:33:08 +01:00
d5d5b2a36d Merge remote-tracking branch 'origin/master' 2024-10-09 15:32:33 +01:00
Lee
25ffd37d0b Merge pull request 'Update dependency lucide-react to ^0.451.0' (#67) from renovate/lucide-monorepo into master
Some checks failed
Deploy Website / deploy (push) Has been cancelled
Reviewed-on: #67
2024-10-09 14:32:10 +00:00
4f0a42472c fix bun fixer (ironic) 2024-10-09 15:32:00 +01:00
Lee
d58a6e0863 Merge pull request 'Update dependency eslint-config-next to v14.2.15' (#69) from renovate/nextjs-monorepo into master
Some checks failed
Deploy Website / deploy (push) Failing after 17s
Reviewed-on: #69
2024-10-09 14:30:52 +00:00
543429852b add bun fixer workflow 2024-10-09 15:30:24 +01:00
ee042fe91e add player tracked since date
All checks were successful
Deploy Backend / deploy (push) Successful in 3m6s
2024-10-09 15:25:21 +01:00
6b8244fa48 fix imports and add a player data refresh interval
All checks were successful
Deploy Website / deploy (push) Successful in 3m53s
2024-10-09 15:20:25 +01:00
094e030f11 auto refresh the player data when claiming a profile
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 3m33s
2024-10-09 15:18:42 +01:00
580665b2f6 fix mongo uri
All checks were successful
Deploy Backend / deploy (push) Successful in 2m19s
2024-10-09 14:57:04 +01:00
b2368dd1d3 update chart labels
All checks were successful
Deploy Website / deploy (push) Successful in 4m16s
2024-10-09 02:44:48 +01:00
a72b098dea re-fix missed notes
All checks were successful
Deploy Website / deploy (push) Successful in 4m20s
2024-10-09 02:38:09 +01:00
c9e102d3d6 re-add beatsaver buttons
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-09 02:35:34 +01:00
946b3c52dc oh it was getting OOM killed
All checks were successful
Deploy Website / deploy (push) Successful in 4m34s
2024-10-09 02:26:24 +01:00
104a08b0d9 idek
All checks were successful
Deploy Website / deploy (push) Successful in 4m32s
2024-10-09 02:20:34 +01:00
cd09148acb Update dependency lucide-react to ^0.451.0 2024-10-09 01:05:35 +00:00
8ec865d985 OOPSIE DOODLE (and fix it crying)
All checks were successful
Deploy Backend / deploy (push) Successful in 4m29s
Deploy Website / deploy (push) Successful in 7m9s
2024-10-09 01:58:42 +01:00
3b7b3b7e50 it builds locally now
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2024-10-09 01:54:07 +01:00
516402863c 7??!?!?
Some checks failed
Deploy Backend / deploy (push) Successful in 3m16s
Deploy Website / deploy (push) Failing after 1m8s
2024-10-09 01:41:45 +01:00
3d2904c6f0 7??!?!?
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
2024-10-09 01:34:34 +01:00
046007af21 7
Some checks failed
Deploy Backend / deploy (push) Failing after 43s
Deploy Website / deploy (push) Has been cancelled
2024-10-09 01:32:52 +01:00
acd5dcd522 7
Some checks failed
Deploy Backend / deploy (push) Failing after 45s
Deploy Website / deploy (push) Failing after 1m0s
2024-10-09 01:27:39 +01:00
6ac3f485f3 oops
Some checks failed
Deploy Backend / deploy (push) Failing after 58s
Deploy Website / deploy (push) Failing after 1m3s
2024-10-09 01:23:56 +01:00
045e811dbb silly lock file 2024-10-09 01:20:57 +01:00
e6600a8b48 oops, build common
Some checks failed
Deploy Backend / deploy (push) Failing after 25s
Deploy Website / deploy (push) Failing after 16s
2024-10-09 01:19:41 +01:00
b6831b4a50 oops, build common 2024-10-09 01:19:31 +01:00
e87d73bbdf LETS GO BABY
Some checks failed
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Has been cancelled
2024-10-09 01:17:00 +01:00
935d4d5589 Update dependency eslint-config-next to v14.2.15 2024-10-08 21:03:20 +00:00
e0fca1168a smh 2024-10-08 16:40:55 +01:00
30bdb07510 fix workflows
Some checks failed
Deploy Backend / deploy (push) Failing after 31s
2024-10-08 16:39:04 +01:00
debe0f13a2 base backend setup 2024-10-08 16:36:52 +01:00
a83c05aa01 Merge remote-tracking branch 'origin/master' 2024-10-08 15:39:17 +01:00
31aad41015 7 2024-10-08 15:39:02 +01:00
0d1cbf4c42 oops
Some checks failed
Deploy Backend / deploy (push) Failing after 47s
2024-10-08 15:33:41 +01:00
3adb895a87 Merge remote-tracking branch 'origin/master'
Some checks failed
Deploy Backend / deploy (push) Failing after 16s
# Conflicts:
#	backend/package.json
#	pnpm-lock.yaml
2024-10-08 15:32:16 +01:00
aa0a0c4c16 start backend work 2024-10-08 15:32:02 +01:00
894 changed files with 12091 additions and 14575 deletions

View File

@ -26,3 +26,14 @@ spec:
limits: limits:
cpu: 1000m # 1 vCPU cpu: 1000m # 1 vCPU
memory: 512Mi memory: 512Mi
env:
- name: MONGO_URI
valueFrom:
secretKeyRef:
name: ssr-backend-secret
key: MONGO_URI
- name: DISCORD_BOT_TOKEN
valueFrom:
secretKeyRef:
name: ssr-backend-secret
key: DISCORD_BOT_TOKEN

View File

@ -24,3 +24,27 @@ spec:
port: 8080 port: 8080
tls: tls:
secretName: fascinated-cc secretName: fascinated-cc
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: scoresaber-reloaded-backend-swagger-ingress
namespace: public-services
annotations:
kubernetes.io/ingress.class: traefik-external
spec:
entryPoints:
- websecure
routes:
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/swagger`)
kind: Rule
middlewares:
- name: default-headers
namespace: traefik
- name: compress
namespace: traefik
services:
- name: scoresaber-reloaded-backend-service
port: 8080
tls:
secretName: fascinated-cc

View File

@ -0,0 +1,17 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: ssr-backend-secret
namespace: public-services
spec:
encryptedData:
DISCORD_BOT_TOKEN: AgCkl523hUe1qei/mybm16Eht0UVEJtPXbcnZYJ6aeLYkB2Mihk8OK+rYGnxN/1MhjYh4VNW36KPZ8/9M/chfsIirECw8OLNhhxzXxbyUpNCmy9JZA4EB5H6UDzC0STOOZekncGtGBg2cU0sZeHaYUlERbFSaDADuLx5aP2RxgHX5xucUfmTtMVbCDg2pCsVT+wliS3gD4FBTVYr1ZxePNCDpPsjzTCd6v+AzlZidIuNwZUDVcXIuCpA+o3jpbzuriMJDyGHA9H7182nZycQ0Q8KQLB1JYgZ2em3Sec5VM4Gkxd6d38c41ldgAIdPjd8rurpxpAdptDgMupmXx8Rm66BdFr0A4ifBpvmLePYOe6cOrjhfGhcEjkzLhBqEWZ7cH4oOKXRX5f0FLWCcKI5G7/vCbRfpr6jx4KuFQt2Mq+BplApVjz/lhqXigd+JDiXx383fYEMaZq8FB2mozfatw1/OpZrhMlunFmQPd/eOcBzJArwYY28qbWaTrKroA1Mc1yWGwlwIEpZAyoNN5x6XuLBKF5tkd+R0h1usDsxQoToHmfYN3qYktZRKsbPb1UpwBHeUPPzzIbgFUgAvGtPhqau4GH3VoCP6qNRt0ATiC8O0k9iXXPrPz9ajp5dzHPmjDz35AmBeoxsrpWZeWbYslc/iIEzqKWBmmXa/NAerJ0fVmMsC9bb+6CTVg7ZOTk0JC9moT5BP3F6U9ZYMkT8x99whvoLdautqnVpnYz/N0SwMVkJzQQKtFNxZH8eTuow8Eqyni2Im1/owMRX00SDtqTt1+8M12GDsnk=
MONGO_URI: AgCDR9Dasy7vtqThemJxCcXEAW+tNXpsSyBQeeF/tpE6Q6L1itl+8f4EEz+WUQ5cZf5HswD105hNZDhEaoqJeDFllmsYyEL7cZMgrsK1UyhY8m6H4+vdAkX4pBiGf0ig5RMkISopEHCyvmdDkO7vvfjsh2fyjBv0BD9KvqMfzg/m3dEbm48JXbBKogdN9H8yj3L4Xi/xaLwgGx8jnG8A0VoR3VmxJfSJKFOhovb5prS0ZByGAT0hrdGb0TgERfFLlsgjrKAiBx1xHgA+L0RPrkJCHXYT7z1ll/LYKfedLqIc76PTYZpMywq8XDIQUu0JnyZb1OzrjhK3zolzb+HIK65Rp4/PoM8yGyUwEbyJ/e6LBjsIjqtVoylYWy3css3xYmABaWD9FVl/qwXekOPJqc1dcOdEItlg34j+H01HXR1p0/SHZOYRTKM1mWNmlvP5nBd6gGue91LUIumonAP/dCUP4KBwaUkvJx+NW5w0L2DXG7ZHn18MUUx/zKgjFGl1GigyD5v/r2gfXXYjTTcdtR0oNHTjB3nBtU0142irJQCuqWuB3NWs8NTPfNp7/kTlHP8eD9NJfiSKCgU+Ld7sg+MBjnB5mVU3dX4WAdebfGZHaa4RIwyJbcvgu88sdSP+bzukcWi5cFppwuVX/4s9Aml+GtvuojmLbzuPAOIGKz5MS8TpB1v3Hiy5VFFO1OiWn53sT093kOp4OxULLWYtlTey9f2LzTbWz+X9oUOdedaVlQW445+eGmyKj9FDWrKk+7+wPhgYNY5EonN6p3OKkWopN3tv/TK3qcvT93uYBbr/sP9VT7GHnkdH2us=
template:
metadata:
creationTimestamp: null
name: ssr-backend-secret
namespace: public-services
type: Opaque

View File

@ -25,30 +25,4 @@ spec:
memory: 128Mi memory: 128Mi
limits: limits:
cpu: 1000m # 1 vCPU cpu: 1000m # 1 vCPU
memory: 256Mi memory: 1024Mi
env:
- name: MONGO_URI
valueFrom:
secretKeyRef:
name: ssr-secret
key: MONGO_URI
- name: NEXT_PUBLIC_SITE_URL
valueFrom:
secretKeyRef:
name: ssr-secret
key: NEXT_PUBLIC_SITE_URL
- name: NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY
valueFrom:
secretKeyRef:
name: ssr-secret
key: NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY
- name: TRIGGER_API_KEY
valueFrom:
secretKeyRef:
name: ssr-secret
key: TRIGGER_API_KEY
- name: TRIGGER_API_URL
valueFrom:
secretKeyRef:
name: ssr-secret
key: TRIGGER_API_URL

View File

@ -1,20 +0,0 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: ssr-secret
namespace: public-services
spec:
encryptedData:
MONGO_URI: AgBDDZhuphvpFZJZq31CA8OmiTr1J8p5Fy/rcr/zvm2nl64GnpbmVCuYgUiH6PwZPCUa9zcyUeZJO9/Xqe9PbJ8hA82j0Pb+Pcl1Rk3+B6jkDaEzcJKDmXS/zx8Q+JPWFOGVpRNy0HCKxm8azy88A9iyiKseIFsMWWrkJMkEObokRBCB4joD9Mh+aOsE2vaUkoE49ASxwVXU9MnL/34eksqGD6D5/BGpVZGftvY/x5eOuhULtK7z3tcd/orc//21AXUSAlVgWcekstEfZWQovk7Rwl67pgpHYf+KuegY4i+0ybge1qEngjvwt76yObTqfmhrdVQNfrV21FpTfoBeZS6ZoHdli6DBanPZgXJdKU2Ttr3C5EJ8c0Gir20J3wRs11SQ1gaKu6bxL4EH4kAtgdVoD5t6MSqvDzkfovAcJUHfXLA2HPhs1CEcu7Y6Kv/v+aGWSlo9jPVQg8JJ7IPF/+DDbF4JgEnwr34e7M5Z/CKVhwm7mK8Nr1yzgEhkucjZ6fcEVmt91fjx1usxDvtN+mllibc7HS2a/ObMDx3MtfHxXhTpt0wXyNyhXtnKNvKbICR5LGZfosF3viNfuRcEFTGvC2Ak9hlhVrznp0FRUiQJSNWsBQKZUKG5Yd5ckQwJUY8B/OLAjg0Keo26LIkciZ0jZD3JtK+bxU5LntdfSCwuO9+xHgaMgCxY+7plPQOgXL9BAOk9Zerc/6xQXJ7y4Q1PogrNaiPn6xCr368utFH2bA4zOAZCwrngnZtmT4pB6r6C/425JoZiQQw6qVQklpC7UhWpV1SbMvdGrlYgBW9TkT5rJPNIE3Bp3kA=
NEXT_PUBLIC_SITE_URL: AgCpMUZ2MFY8mHgQ3fizTzcBImnwFmWzccRCtMAThI0cAIOcDe15Drk2a5a4UjcYgl1F+JrHB3b3IPbflr1E4dNAANKRgiGW+gyI2S7J/oDpb+ANCv/0RJIlfQh9Pcb/E4noKVOoUfe4dg5asq1kQjOob4uOn6MfQXoC5WfgK8u8q0T5tEPcuGxXt2Q1OnyAAWm/0Z7JSLfgQN2sKaAbRbWqKfwfsc4LgjxY98m/+BkXN7x6R7BJmXXMd0cb5ctdgM1ZpU+gYhhwyO0xsxYWURcJb9EsrNZR6OY4DbwXw2tpoagFxA20u5J2ZUhUeVRg2x2R5AdkL7OBIT73Xbh3WxIYVAqGDhs90aRrmlCdr61eBLCLtytC33LJ/6Odq2Pa9DLaKqRlqRX/IWk7+cgHOKfSd8/k5R1roA3A96ShFby9RdXGudGLA2G4dvLtrruLCYVRfxMJB2k3UYtGZB21o+3SAV0jx/83eoYzoBGHM6K8ySCpL1uDCo8ATL2iYJcacgYZGKaGxBumzEjAMBqTLBSUl0Jhx3mr59p6mrYKFtbewa9rJUOkNniYvdCeokLyVntxUMx60Jtrtg05G3vSFaP34Gp6Oq6J0jSzvYi/A3/iSe+cNB1fpNJvJVLRFmJ6f7qyMMoSujIoql5SfIhx/tyUHueiOFQ5KXKTeNhbu6byakY1ZHa2o03+Mooca2ATwUnlNNi73sKluFKhnRysANIiVoRZLDQniLwV
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY: AgCZwhhUNhSMwuR1pCP5qDY9fD99u78PFq89ej141pc/L/y/UCydLvftFKT62bXzIFhoq77dlU3yFx2FqbApdiDv3sDltZkIQh/afYwySPw3bXxoQoHcAix5qGhWrpDkPFDOi+sJkkPnnZC1OBncrqz8xAwfYAhwOscW9mjugRMJPynqSlnVHS1RdYm6z7eSJpZEMEHIT4tptPnzP+icRwbolgKL66JXFXvuS6SnTZ+ZOtub39L+wpWE9dQ83E5YqtWl3hci2G+rK9KBk89zuBM7Ho+MTpcdcaes64ApMqaUnFPelqJKSk6PK7mEX9DZhCUqNyCu897ktfHKulVZQ5Wy2+pVHXx9e1IBI7YqNph64CbX6N0V6ABfNlO2sS+zFG3dGuEGj/lI9hfSxqQauYOWXR7r8zM86WvNuxWuQFQbO4B1TDd8oofhZ+wwcUfJ0/pZIqyxcINB13opF107wa4MlfoCI6sgB4/adq/bbMP/JO10/GBiuJRhE63NhVJEZovJoRNV2+wBRNSVRfZpEQ9AXSACm1BtqOxhYhAmDnJt6ThF6VDWB2ZoDZfWul/kPUTUiOulGHmsRdn/bzTS8GjhY93G1/FpNmhNSOC8YbO3FDw8vXg2Vy6jpdKOhy08H9R/9UqbiHxnXPyBGyoizbnjP0sDx4jYYXtix03ZPFf6Dxz6iwwy5BbHpk9Ik+3l2iKI7IcxOOS9P8ljlsB0cCivpTax1iuDZ4hlJ7zm
TRIGGER_API_KEY: AgARH8DdSu8INQ2OW6I4s2W+HZqHGZHn0i54l02Ui48Oph9koB6pfTvAkYspQ6LI2zh/R/uiAeOHorybTMZ9X0EEwk5GxTuXBUn4f5Ifpd2QkoHeDVWP6MA951PVanfPuXLklwKJm2O70oFKIVE61v52yZbk0L3wAOiYdRTj0igrSEDkmmc9iHorGdbDCI3CkZHpOMMl37zdIwCvbpHaCnSBpKEuQ0PmvRtAw9ydM3FhVpTxNVh3KhTgvGBBYwrGXOZuKOayLGvQ16pYmTSPoN6DNRFSLjmE/BOjwKnYfZU0C0qkpGPlNLSUteuLLvHtzlS8IOSboOspreQJMVaSRpg+Qp1/cV0XGEhmU/CWVTYqkNx5QtfgaxWllrKrQxNW0WMDJmnQI83scsAiweSFUffsfiX8BCMjHkD2nvlXCz6vzUcJ7Zn0bDPoHcv/uG7efZbsJXLie1PxQiGwFYpuyr0b7+A+RVgx0G/WNwKJIUjFC7acI7jY4dGE04zKe1STYhMhoc1gjKGhXe0BG73LAX/O5/x6W4iYUyc4n0HL7gLwlbpfR3zLkvuiiAtzFeKGRr+SF24mj95pfw+MPFoKEi9htLdPgHxTYomfQ+1I8R7Iya0sHtyW2fI/1e5XzJOMHub/tYh5y9h0UqE5n7ByapRMyj0mOrKXXPUoT4btQDz0U6aNRX+MrlwMsuXYjSfUCuXmy30RKQImmT+9vaukIq1CX7WJ2LQ8fHaYACnp
TRIGGER_API_URL: AgAOwyGxQEScm5T3Hh1armqqcEcMEo0v5Mwf9JjEf3G+3svlDDPGHlyHdQolcC2YlkX7DhsenEp6rokh1grwyVoruyUc6OmRdRR70+PV5qMgSC3HY6lZ5f2gcGfA0uh9A5sm4qkOw4rliRddpJqKOqDz28zcrcu5RmusPxric+KF6Hcdy+ugqmq0KZl9VU2+D4z3QWkdokHk3WahdLneS4a3bHYC/NIpKyI5SveK6QAaQlU3NXrqKcof6VzDQG20bnCKGo+Y935LgzEIEmWKw2C9lwCV+/RUIjeaK2qzZpeMiZue9zgoq1dyNNjrar9B6zb+rSxcgnbqBolXUAVk1If3+egVNEaB9SjU22n+WoTA6HK1MOSwsaMtf1Tug/8nSQfFHdw1nZzBVtiVaFMtzmg0aKyrUpAYyz4XTn6xn9EhEKgcPSaWINf4zVcmceLOYenOP/y7S3cVx9KHBjUNGf/eDJVmXSiOzeguIJBfdEOla/lqv7Zx2/wvfHeEdurn5ENTkG2aQAekIvWiJ1HzPwrKKR6WcBpNTgjoDRMNxVoMcZ3QB9iJlp3AoLfJW72B2soTVeIikcNlT0Q0S91hiqvEcE+WuE5bDSttzhnb9nvEJXz6gC6AykCKH1VLIJJuiMI6R7V9z8fo7pFVXbQsM10VUph/9vxhib2XZ1c/25YMfj5vaI1+N7UiVDFlEfE2YJQMwd2vj+wTa3wHJz+2KXc/9rhBoaIpznN4LmKR6g==
template:
metadata:
creationTimestamp: null
name: ssr-secret
namespace: public-services
type: Opaque

View File

@ -1,4 +1,4 @@
name: "Deploy Backend" name: Deploy Backend
on: on:
workflow_dispatch: workflow_dispatch:
@ -6,58 +6,93 @@ on:
branches: branches:
- master - master
paths: paths:
- backend/** - projects/backend/**
- common/** - projects/common/**
- .gitea/kubernetes/backend/**
- .gitea/workflows/deploy-backend.yml - .gitea/workflows/deploy-backend.yml
jobs: jobs:
deploy: docker:
runs-on: ubuntu-latest strategy:
matrix:
arch: ["ubuntu-latest"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps: steps:
- name: Checkout code # Checkout the repo
- name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with: with:
username: ${{ secrets.REGISTRY_USERNAME }} fetch-depth: 0
password: ${{ secrets.REGISTRY_TOKEN }}
registry: git.fascinated.cc
- name: Build Image # Deploy to Dokku
uses: docker/build-push-action@v6 - name: Push to dokku
uses: dokku/github-action@master
with: with:
context: . git_remote_url: "ssh://dokku@51.158.63.74:22/ssr-backend"
file: ./backend/Dockerfile ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
push: true
tags: |
git.fascinated.cc/fascinated/scoresaber-reloaded-backend:${{ github.sha }}
git.fascinated.cc/fascinated/scoresaber-reloaded-backend:latest
build-args: |
GIT_REV=${{ gitea.sha }}
- name: Install kubectl #name: "Deploy Backend"
uses: azure/setup-kubectl@v4 #
id: install #on:
# workflow_dispatch:
- name: Setup Kubernetes Context # push:
uses: azure/k8s-set-context@v4 # branches:
with: # - master
kubeconfig: ${{ secrets.KUBECONFIG }} # paths:
# - projects/backend/**
- name: Deploy to Kubernetes # - projects/common/**
uses: Azure/k8s-deploy@v5 # - .gitea/kubernetes/backend/**
with: # - .gitea/workflows/deploy-backend.yml
action: deploy #
namespace: public-services #jobs:
manifests: | # deploy:
.gitea/kubernetes/backend/deployment.yaml # runs-on: ubuntu-latest
.gitea/kubernetes/backend/service.yaml # steps:
.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml # - name: Checkout code
.gitea/kubernetes/backend/ingress.yaml # uses: actions/checkout@v4
images: | #
git.fascinated.cc/fascinated/scoresaber-reloaded-backend:${{ github.sha }} # - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
#
# - name: Login to Docker Hub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.REGISTRY_USERNAME }}
# password: ${{ secrets.REGISTRY_TOKEN }}
# registry: git.fascinated.cc
#
# - name: Build Image
# uses: docker/build-push-action@v6
# with:
# context: .
# file: ./projects/backend/Dockerfile
# push: true
# tags: |
# git.fascinated.cc/fascinated/scoresaber-reloaded-backend:${{ github.sha }}
# git.fascinated.cc/fascinated/scoresaber-reloaded-backend:latest
# build-args: |
# GIT_REV=${{ gitea.sha }}
#
# - name: Install kubectl
# uses: azure/setup-kubectl@v4
# id: install
#
# - name: Setup Kubernetes Context
# uses: azure/k8s-set-context@v4
# with:
# kubeconfig: ${{ secrets.KUBECONFIG }}
#
# - name: Deploy to Kubernetes
# uses: Azure/k8s-deploy@v5
# with:
# action: deploy
# namespace: public-services
# manifests: |
# .gitea/kubernetes/backend/sealed-secret.yaml
# .gitea/kubernetes/backend/deployment.yaml
# .gitea/kubernetes/backend/service.yaml
# .gitea/kubernetes/backend/strip-api-prefix-middleware.yaml
# .gitea/kubernetes/backend/ingress.yaml
# images: |
# git.fascinated.cc/fascinated/scoresaber-reloaded-backend:${{ github.sha }}

View File

@ -1,4 +1,4 @@
name: "Deploy Website" name: Deploy Website
on: on:
workflow_dispatch: workflow_dispatch:
@ -6,59 +6,93 @@ on:
branches: branches:
- master - master
paths: paths:
- website/** - projects/website/**
- common/** - projects/common/**
- .gitea/kubernetes/website/**
- .gitea/workflows/deploy-website.yml - .gitea/workflows/deploy-website.yml
- bun.lockb
jobs: jobs:
deploy: docker:
runs-on: ubuntu-latest strategy:
matrix:
arch: ["ubuntu-latest"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps: steps:
- name: Checkout code # Checkout the repo
- name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with: with:
username: ${{ secrets.REGISTRY_USERNAME }} fetch-depth: 0
password: ${{ secrets.REGISTRY_TOKEN }}
registry: git.fascinated.cc
- name: Build Image # Deploy to Dokku
uses: docker/build-push-action@v6 - name: Push to dokku
uses: dokku/github-action@master
with: with:
context: . git_remote_url: "ssh://dokku@51.158.63.74:22/ssr-website"
file: ./website/Dockerfile ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
push: true
tags: |
git.fascinated.cc/fascinated/scoresaber-reloaded-website:${{ github.sha }}
git.fascinated.cc/fascinated/scoresaber-reloaded-website:latest
build-args: |
GIT_REV=${{ gitea.sha }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Install kubectl #name: "Deploy Website"
uses: azure/setup-kubectl@v4 #
id: install #on:
# workflow_dispatch:
- name: Setup Kubernetes Context # push:
uses: azure/k8s-set-context@v4 # branches:
with: # - master
kubeconfig: ${{ secrets.KUBECONFIG }} # paths:
# - projects/website/**
- name: Deploy to Kubernetes # - projects/common/**
uses: Azure/k8s-deploy@v5 # - .gitea/kubernetes/website/**
with: # - .gitea/workflows/deploy-website.yml
action: deploy #
namespace: public-services #jobs:
manifests: | # deploy:
.gitea/kubernetes/website/sealed-secrets.yaml # runs-on: ubuntu-latest
.gitea/kubernetes/website/deployment.yaml # steps:
.gitea/kubernetes/website/service.yaml # - name: Checkout code
.gitea/kubernetes/website/ingress.yaml # uses: actions/checkout@v4
images: | #
git.fascinated.cc/fascinated/scoresaber-reloaded-website:${{ github.sha }} # - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
#
# - name: Login to Docker Hub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.REGISTRY_USERNAME }}
# password: ${{ secrets.REGISTRY_TOKEN }}
# registry: git.fascinated.cc
#
# - name: Build Image
# uses: docker/build-push-action@v6
# with:
# context: .
# file: ./projects/website/Dockerfile
# push: true
# tags: |
# git.fascinated.cc/fascinated/scoresaber-reloaded-website:${{ github.sha }}
# git.fascinated.cc/fascinated/scoresaber-reloaded-website:latest
# build-args: |
# GIT_REV=${{ gitea.sha }}
# SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
#
# - name: Install kubectl
# uses: azure/setup-kubectl@v4
# id: install
#
# - name: Setup Kubernetes Context
# uses: azure/k8s-set-context@v4
# with:
# kubeconfig: ${{ secrets.KUBECONFIG }}
#
# - name: Deploy to Kubernetes
# uses: Azure/k8s-deploy@v5
# with:
# action: deploy
# namespace: public-services
# manifests: |
# .gitea/kubernetes/website/deployment.yaml
# .gitea/kubernetes/website/service.yaml
# .gitea/kubernetes/website/ingress.yaml
# images: |
# git.fascinated.cc/fascinated/scoresaber-reloaded-website:${{ github.sha }}

2
.gitignore vendored
View File

@ -77,3 +77,5 @@ sketch
.env*.local .env*.local
.vercel .vercel
next-env.d.ts next-env.d.ts
.idea
secret.yaml

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
bun.lockb Normal file

Binary file not shown.

View File

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

View File

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

View File

@ -1,6 +0,0 @@
/**
* Checks if we're in production
*/
export function isProduction() {
return process.env.NODE_ENV === "production";
}

View File

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

View File

@ -1,9 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
splitting: false,
sourcemap: true,
clean: true,
dts: true, // This line enables type declaration file generation
});

View File

@ -1,15 +1,19 @@
{ {
"name": "scoresaber-reloadedv3", "name": "scoresaber-reloaded",
"version": "1.0.0", "version": "1.0.0",
"workspaces": [
"projects/*"
],
"scripts": { "scripts": {
"dev": "pnpm --parallel --workspace-concurrency=4 run -r dev", "dev:website": "bun --filter 'website' dev",
"dev:backend": "bun --filter 'backend' dev",
"build:website": "pnpm --filter website build", "dev:common": "bun --filter '@ssr/common' dev",
"build:backend": "pnpm --filter backend build", "dev": "concurrently \"bun run dev:common\" \"bun run dev:backend\""
"start:website": "pnpm --filter website start",
"start:backend": "pnpm --filter backend start"
}, },
"author": "fascinated7", "author": "fascinated7",
"license": "MIT" "license": "MIT",
"dependencies": {
"concurrently": "^9.0.1",
"cross-env": "^7.0.3"
}
} }

10709
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,2 @@
MONGO_URI=mongodb://localhost:27017
API_URL=http://localhost:8080

View File

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

44
projects/backend/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,34 @@
FROM oven/bun:1.1.33-alpine AS base
# Install dependencies
FROM base AS depends
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
# Run the app
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Copy the depends
COPY --from=depends /app/package.json* /app/bun.lockb* ./
COPY --from=depends /app/node_modules ./node_modules
# Build the common library
COPY --from=depends /app/projects/common ./projects/common
RUN bun i -g typescript
RUN bun --filter '@ssr/common' build
# Copy the backend project
COPY --from=depends /app/projects/backend ./projects/backend
# Lint before starting
RUN bun --filter 'backend' lint
ARG PORT=8080
ENV PORT $PORT
EXPOSE $PORT
CMD ["bun", "run", "--filter", "backend", "start"]

View File

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

View File

@ -0,0 +1,20 @@
import React from "react";
export const GlobeIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 26"
fill="currentColor"
style={{
width: "33px",
height: "33px",
paddingRight: "3px",
}}
>
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25ZM6.262 6.072a8.25 8.25 0 1 0 10.562-.766 4.5 4.5 0 0 1-1.318 1.357L14.25 7.5l.165.33a.809.809 0 0 1-1.086 1.085l-.604-.302a1.125 1.125 0 0 0-1.298.21l-.132.131c-.439.44-.439 1.152 0 1.591l.296.296c.256.257.622.374.98.314l1.17-.195c.323-.054.654.036.905.245l1.33 1.108c.32.267.46.694.358 1.1a8.7 8.7 0 0 1-2.288 4.04l-.723.724a1.125 1.125 0 0 1-1.298.21l-.153-.076a1.125 1.125 0 0 1-.622-1.006v-1.089c0-.298-.119-.585-.33-.796l-1.347-1.347a1.125 1.125 0 0 1-.21-1.298L9.75 12l-1.64-1.64a6 6 0 0 1-1.676-3.257l-.172-1.03Z"
clipRule="evenodd"
/>
</svg>
);

View File

@ -0,0 +1,20 @@
import React from "react";
export const StarIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
style={{
width: "40px",
height: "40px",
paddingRight: "3px",
}}
>
<path
fillRule="evenodd"
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
clipRule="evenodd"
/>
</svg>
);

View File

@ -0,0 +1,39 @@
{
"name": "backend",
"version": "1.0.0",
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@bogeychan/elysia-etag": "^0.0.6",
"@dotenvx/dotenvx": "^1.16.1",
"@elysiajs/cors": "^1.1.1",
"@elysiajs/cron": "^1.1.1",
"@elysiajs/swagger": "^1.1.3",
"@ssr/common": "workspace:common",
"@tqman/nice-logger": "^1.0.1",
"@typegoose/auto-increment": "^4.7.0",
"@typegoose/typegoose": "^12.8.0",
"@typescript-eslint/eslint-plugin": "^8.9.0",
"@typescript-eslint/parser": "^8.9.0",
"@vercel/og": "^0.6.3",
"discordx": "^11.12.1",
"elysia": "latest",
"elysia-autoroutes": "^0.5.0",
"elysia-decorators": "^1.0.2",
"elysia-helmet": "^2.0.0",
"eslint": "^8.57.1",
"extract-colors": "^4.1.0",
"jimp": "^1.6.0",
"ky": "^1.7.2",
"mongoose": "^8.7.0",
"node-cache": "^5.1.2",
"react": "^18.3.1"
},
"devDependencies": {
"bun-types": "latest"
},
"module": "src/index.js"
}

View File

@ -0,0 +1,65 @@
import { Client, MetadataStorage } from "discordx";
import { ActivityType, EmbedBuilder } from "discord.js";
import { Config } from "@ssr/common/config";
export const guildId = "1295984874942894100";
export enum DiscordChannels {
trackedPlayerLogs = "1295985197262569512",
numberOneFeed = "1295988063817830430",
backendLogs = "1296524935237468250",
}
const client = new Client({
intents: ["Guilds", "GuildMessages"],
presence: {
status: "online",
activities: [
{
name: "scores...",
type: ActivityType.Watching,
url: "https://ssr.fascinated.cc",
},
],
},
});
client.once("ready", () => {
console.log("Discord bot ready!");
});
export async function initDiscordBot() {
console.log("Initializing discord bot...");
// We will now build our application to load all the commands/events for both bots.
MetadataStorage.instance.build().then(async () => {
// Setup slash commands
client.once("ready", async () => {
await client.initApplicationCommands();
console.log(client.applicationCommands);
});
client.on("interactionCreate", interaction => {
client.executeInteraction(interaction);
});
// Login
await client.login(Config.discordBotToken!);
});
}
/**
* Logs the message to a discord channel.
*
* @param channelId the channel id to log to
* @param message the message to log
*/
export async function logToChannel(channelId: DiscordChannels, message: EmbedBuilder) {
try {
const channel = await client.channels.fetch(channelId);
if (channel != undefined && channel.isSendable()) {
channel.send({ embeds: [message] });
}
} catch {
/* empty */
}
}

View File

@ -0,0 +1,20 @@
import { Discord, Guild, Slash } from "discordx";
import { CommandInteraction } from "discord.js";
import { PlayerService } from "../../service/player.service";
import { guildId } from "../bot";
@Discord()
export class RefreshPlayerScoresCommand {
@Guild(guildId)
@Slash({
description: "Refreshes scores for all tracked players",
name: "refresh-player-scores",
defaultMemberPermissions: ["Administrator"],
})
hello(interaction: CommandInteraction) {
interaction.reply("Updating player scores...").then(async response => {
await PlayerService.refreshPlayerScores();
await response.edit("Done!");
});
}
}

View File

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

View File

@ -0,0 +1,35 @@
import { SSRCache } from "@ssr/common/cache";
import { InternalServerError } from "@ssr/common/error/internal-server-error";
import { isProduction } from "@ssr/common/utils/utils";
/**
* Fetches data with caching.
*
* @param cache the cache to fetch from
* @param cacheKey The key used for caching.
* @param fetchFn The function to fetch data if it's not in cache.
*/
export async function fetchWithCache<T>(
cache: SSRCache,
cacheKey: string,
fetchFn: () => Promise<T | undefined>
): Promise<T | undefined> {
if (!isProduction()) {
return await fetchFn();
}
if (cache == undefined) {
throw new InternalServerError(`Cache is not defined`);
}
if (cache.has(cacheKey)) {
return cache.get<T>(cacheKey);
}
const data = await fetchFn();
if (data) {
cache.set(cacheKey, data);
}
return data;
}

View File

@ -0,0 +1,37 @@
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { DiscordChannels, logToChannel } from "../bot/bot";
import { EmbedBuilder } from "discord.js";
import { formatPp } from "@ssr/common/utils/number-utils";
/**
* Logs that a new player is being tracked
*
* @param player the player being tracked
*/
export async function logNewTrackedPlayer(player: ScoreSaberPlayerToken) {
await logToChannel(
DiscordChannels.trackedPlayerLogs,
new EmbedBuilder()
.setTitle("New Player Tracked")
.setDescription(`https://ssr.fascinated.cc/player/${player.id}`)
.addFields([
{
name: "Username",
value: player.name,
inline: true,
},
{
name: "ID",
value: player.id,
inline: true,
},
{
name: "PP",
value: formatPp(player.pp) + "pp",
inline: true,
},
])
.setThumbnail(player.profilePicture)
.setColor("#00ff00")
);
}

View File

@ -0,0 +1,26 @@
import { Controller, Get } from "elysia-decorators";
import { getAppVersion } from "../common/app.util";
import { AppService } from "../service/app.service";
@Controller()
export default class AppController {
@Get("/")
public async index() {
return {
app: "backend",
version: await getAppVersion(),
};
}
@Get("/health")
public async getHealth() {
return {
status: "OK",
};
}
@Get("/statistics")
public async getStatistics() {
return await AppService.getAppStatistics();
}
}

View File

@ -0,0 +1,36 @@
import { Controller, Get } from "elysia-decorators";
import { t } from "elysia";
import { ImageService } from "../service/image.service";
@Controller("/image")
export default class ImageController {
@Get("/averagecolor/:url", {
config: {},
params: t.Object({
url: t.String({ required: true }),
}),
})
public async getImageAverageColor({ params: { url } }: { params: { url: string } }) {
return await ImageService.getAverageImageColor(url);
}
@Get("/player/:id", {
config: {},
params: t.Object({
id: t.String({ required: true }),
}),
})
public async getPlayerImage({ params: { id } }: { params: { id: string } }) {
return await ImageService.generatePlayerImage(id);
}
@Get("/leaderboard/:id", {
config: {},
params: t.Object({
id: t.String({ required: true }),
}),
})
public async getLeaderboardImage({ params: { id } }: { params: { id: string } }) {
return await ImageService.generateLeaderboardImage(id);
}
}

View File

@ -0,0 +1,26 @@
import { Controller, Get } from "elysia-decorators";
import { t } from "elysia";
import { Leaderboards } from "@ssr/common/leaderboard";
import LeaderboardService from "../service/leaderboard.service";
@Controller("/leaderboard")
export default class LeaderboardController {
@Get("/:leaderboard/:id", {
config: {},
params: t.Object({
id: t.String({ required: true }),
leaderboard: t.String({ required: true }),
}),
})
public async getLeaderboard({
params: { leaderboard, id },
}: {
params: {
leaderboard: Leaderboards;
id: string;
page: number;
};
}): Promise<unknown> {
return await LeaderboardService.getLeaderboard(leaderboard, id);
}
}

View File

@ -0,0 +1,80 @@
import { Controller, Get } from "elysia-decorators";
import { PlayerService } from "../service/player.service";
import { t } from "elysia";
import { PlayerHistory } from "@ssr/common/player/player-history";
import { PlayerTrackedSince } from "@ssr/common/player/player-tracked-since";
import { AroundPlayerResponse } from "@ssr/common/response/around-player-response";
@Controller("/player")
export default class PlayerController {
@Get("/history/:id/:days", {
config: {},
params: t.Object({
id: t.String({ required: true }),
days: t.Number({ default: 50, required: false }),
}),
query: t.Object({
createIfMissing: t.Boolean({ default: false, required: false }),
}),
})
public async getPlayer({
params: { id, days },
query: { createIfMissing },
}: {
params: { id: string; days: number };
query: { createIfMissing: boolean };
}): Promise<{ statistics: Record<string, PlayerHistory> }> {
if (days < 1) {
days = 1;
}
// Limit to 10 years
if (days > 365 * 10) {
days = 365 * 10;
}
const player = await PlayerService.getPlayer(id, createIfMissing);
return { statistics: player.getHistoryPreviousDays(days) };
}
@Get("/tracked/:id", {
config: {},
params: t.Object({
id: t.String({ required: true }),
}),
})
public async getTrackedStatus({
params: { id },
query: { createIfMissing },
}: {
params: { id: string };
query: { createIfMissing: boolean };
}): Promise<PlayerTrackedSince> {
try {
const player = await PlayerService.getPlayer(id, createIfMissing);
return {
tracked: true,
daysTracked: player.getDaysTracked(),
};
} catch {
return {
tracked: false,
};
}
}
@Get("/around/:id/:type", {
config: {},
params: t.Object({
id: t.String({ required: true }),
type: t.String({ required: true }),
}),
})
public async getPlayersAround({
params: { id, type },
}: {
params: { id: string; type: "global" | "country" };
}): Promise<AroundPlayerResponse> {
return {
players: await PlayerService.getPlayersAroundPlayer(id, type),
};
}
}

View File

@ -0,0 +1,104 @@
import { Controller, Get } from "elysia-decorators";
import { t } from "elysia";
import { Leaderboards } from "@ssr/common/leaderboard";
import { TopScoresResponse } from "@ssr/common/response/top-scores-response";
import { ScoreService } from "../service/score.service";
import { Timeframe } from "@ssr/common/timeframe";
@Controller("/scores")
export default class ScoresController {
@Get("/player/:leaderboard/:id/:page/:sort", {
config: {},
params: t.Object({
leaderboard: t.String({ required: true }),
id: t.String({ required: true }),
page: t.Number({ required: true }),
sort: t.String({ required: true }),
}),
query: t.Object({
search: t.Optional(t.String()),
}),
})
public async getScores({
params: { leaderboard, id, page, sort },
query: { search },
}: {
params: {
leaderboard: Leaderboards;
id: string;
page: number;
sort: string;
};
query: { search?: string };
}): Promise<unknown> {
return await ScoreService.getPlayerScores(leaderboard, id, page, sort, search);
}
@Get("/leaderboard/:leaderboard/:id/:page", {
config: {},
params: t.Object({
leaderboard: t.String({ required: true }),
id: t.String({ required: true }),
page: t.Number({ required: true }),
}),
})
public async getLeaderboardScores({
params: { leaderboard, id, page },
}: {
params: {
leaderboard: Leaderboards;
id: string;
page: number;
};
query: { search?: string };
}): Promise<unknown> {
return await ScoreService.getLeaderboardScores(leaderboard, id, page);
}
@Get("/history/:playerId/:leaderboardId/:page", {
config: {},
params: t.Object({
playerId: t.String({ required: true }),
leaderboardId: t.String({ required: true }),
page: t.Number({ required: true }),
}),
})
public async getScoreHistory({
params: { playerId, leaderboardId, page },
}: {
params: {
playerId: string;
leaderboardId: string;
page: number;
};
query: { search?: string };
}): Promise<unknown> {
return (await ScoreService.getScoreHistory(playerId, leaderboardId, page)).toJSON();
}
@Get("/top", {
config: {},
query: t.Object({
limit: t.Number({ required: true }),
timeframe: t.String({ required: true }),
}),
})
public async getTopScores({
query: { limit, timeframe },
}: {
query: { limit: number; timeframe: Timeframe };
}): Promise<TopScoresResponse> {
if (limit <= 0) {
limit = 1;
} else if (limit > 100) {
limit = 100;
}
const scores = await ScoreService.getTopScores(limit, timeframe);
return {
scores,
timeframe,
limit,
};
}
}

View File

@ -0,0 +1,176 @@
import { Elysia } from "elysia";
import cors from "@elysiajs/cors";
import { decorators } from "elysia-decorators";
import { logger } from "@tqman/nice-logger";
import { swagger } from "@elysiajs/swagger";
import { helmet } from "elysia-helmet";
import { etag } from "@bogeychan/elysia-etag";
import AppController from "./controller/app.controller";
import * as dotenv from "@dotenvx/dotenvx";
import mongoose from "mongoose";
import PlayerController from "./controller/player.controller";
import { PlayerService } from "./service/player.service";
import { cron } from "@elysiajs/cron";
import { isProduction } from "@ssr/common/utils/utils";
import ImageController from "./controller/image.controller";
import { ScoreService } from "./service/score.service";
import { Config } from "@ssr/common/config";
import ScoresController from "./controller/scores.controller";
import LeaderboardController from "./controller/leaderboard.controller";
import { getAppVersion } from "./common/app.util";
import { connectScoresaberWebsocket } from "@ssr/common/websocket/scoresaber-websocket";
import { connectBeatLeaderWebsocket } from "@ssr/common/websocket/beatleader-websocket";
import { DiscordChannels, initDiscordBot, logToChannel } from "./bot/bot";
import { EmbedBuilder } from "discord.js";
// Load .env file
dotenv.config({
logLevel: (await Bun.file(".env").exists()) ? "success" : "warn",
path: ".env",
override: true,
});
// Connect to Mongo
await mongoose.connect(Config.mongoUri!); // Connect to MongoDB
// Connect to websockets
connectScoresaberWebsocket({
onScore: async score => {
await ScoreService.trackScoreSaberScore(score.score, score.leaderboard);
await ScoreService.updatePlayerScoresSet(score);
await ScoreService.notifyNumberOne(score);
},
onDisconnect: async error => {
await logToChannel(
DiscordChannels.backendLogs,
new EmbedBuilder().setDescription(`ScoreSaber websocket disconnected: ${JSON.stringify(error)}`)
);
},
});
connectBeatLeaderWebsocket({
onScore: async score => {
await ScoreService.trackBeatLeaderScore(score);
},
onDisconnect: async error => {
await logToChannel(
DiscordChannels.backendLogs,
new EmbedBuilder().setDescription(`BeatLeader websocket disconnected: ${JSON.stringify(error)}`)
);
},
});
export const app = new Elysia();
app.use(
cron({
name: "player-statistics-tracker-cron",
pattern: "0 1 * * *", // Every day at 00:01
timezone: "Europe/London", // UTC time
protect: true,
run: async () => {
await PlayerService.updatePlayerStatistics();
},
})
);
app.use(
cron({
name: "player-scores-tracker-cron",
pattern: "0 4 * * *", // Every day at 04:00
timezone: "Europe/London", // UTC time
protect: true,
run: async () => {
await PlayerService.refreshPlayerScores();
},
})
);
/**
* Custom error handler
*/
app.onError({ as: "global" }, ({ code, error }) => {
// Return default error for type validation
if (code === "VALIDATION") {
return error.all;
}
const status = "status" in error ? error.status : undefined;
return {
...((status && { statusCode: status }) || { status: code }),
...(error.message != code && { message: error.message }),
timestamp: new Date().toISOString(),
};
});
/**
* Enable E-Tags
*/
app.use(etag());
/**
* Enable CORS
*/
app.use(cors());
/**
* Request logger
*/
app.use(
logger({
enabled: true,
mode: "combined",
})
);
/**
* Security settings
*/
app.use(
helmet({
hsts: false, // Disable HSTS
contentSecurityPolicy: false, // Disable CSP
dnsPrefetchControl: true, // Enable DNS prefetch
})
);
/**
* Controllers
*/
app.use(
decorators({
controllers: [AppController, PlayerController, ImageController, ScoresController, LeaderboardController],
})
);
/**
* Swagger Documentation
*/
app.use(
swagger({
documentation: {
info: {
title: "ScoreSaber Reloaded Documentation",
version: await getAppVersion(),
},
},
scalarConfig: {
servers: [
{
url: "https://ssr.fascinated.cc/api",
description: "Production server",
},
],
},
})
);
app.onStart(async () => {
console.log("Listening on port http://localhost:8080");
if (isProduction()) {
await initDiscordBot();
}
});
app.listen({
port: 8080,
idleTimeout: 120, // 2 minutes
});

View File

@ -0,0 +1,38 @@
import { PlayerModel } from "@ssr/common/model/player";
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
import { ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
import { AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data/additional-score-data";
import { BeatSaverMapModel } from "@ssr/common/model/beatsaver/map";
import { ScoreSaberLeaderboardModel } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
import { SSRCache } from "@ssr/common/cache";
const statisticsCache = new SSRCache({
ttl: 120 * 1000, // 2 minutes
});
export class AppService {
/**
* Gets the app statistics.
*/
public static async getAppStatistics(): Promise<AppStatistics> {
if (statisticsCache.has("app-statistics")) {
return statisticsCache.get<AppStatistics>("app-statistics")!;
}
const trackedPlayers = await PlayerModel.countDocuments();
const trackedScores = await ScoreSaberScoreModel.countDocuments();
const additionalScoresData = await AdditionalScoreDataModel.countDocuments();
const cachedBeatSaverMaps = await BeatSaverMapModel.countDocuments();
const cachedScoreSaberLeaderboards = await ScoreSaberLeaderboardModel.countDocuments();
const response = {
trackedPlayers,
trackedScores,
additionalScoresData,
cachedBeatSaverMaps,
cachedScoreSaberLeaderboards,
};
statisticsCache.set("app-statistics", response);
return response;
}
}

View File

@ -0,0 +1,98 @@
import { beatsaverService } from "@ssr/common/service/impl/beatsaver";
import { BeatSaverMap, BeatSaverMapModel } from "@ssr/common/model/beatsaver/map";
export default class BeatSaverService {
/**
* Gets a map by its hash, updates if necessary, or inserts if not found.
*
* @param hash the hash of the map
* @returns the beatsaver map, or undefined if not found
*/
public static async getMap(hash: string): Promise<BeatSaverMap | undefined> {
let map = await BeatSaverMapModel.findOne({
"versions.hash": hash.toUpperCase(),
});
if (map) {
const toObject = map.toObject() as BeatSaverMap;
// If the map is not found, return undefined
if (toObject.notFound) {
return undefined;
}
// If the map does not need to be refreshed, return it
if (!(map as unknown as BeatSaverMap).shouldRefresh()) {
return toObject;
}
}
// Map needs to be fetched or refreshed
const token = await beatsaverService.lookupMap(hash);
const uploader = token?.uploader;
const metadata = token?.metadata;
// Create the new map object based on fetched data
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const newMapData: BeatSaverMap =
token && uploader && metadata
? {
_id: hash, // todo: change this to an incrementing id
bsr: token.id,
name: token.name,
description: token.description,
author: {
id: uploader.id,
name: uploader.name,
avatar: uploader.avatar,
},
metadata: {
bpm: metadata.bpm,
duration: metadata.duration,
levelAuthorName: metadata.levelAuthorName,
songAuthorName: metadata.songAuthorName,
songName: metadata.songName,
songSubName: metadata.songSubName,
},
versions: token.versions.map(version => ({
hash: version.hash.toUpperCase(),
difficulties: version.diffs.map(diff => ({
njs: diff.njs,
offset: diff.offset,
notes: diff.notes,
bombs: diff.bombs,
obstacles: diff.obstacles,
nps: diff.nps,
characteristic: diff.characteristic,
difficulty: diff.difficulty,
events: diff.events,
chroma: diff.chroma,
mappingExtensions: diff.me,
noodleExtensions: diff.ne,
cinema: diff.cinema,
maxScore: diff.maxScore,
label: diff.label,
})),
createdAt: new Date(version.createdAt),
})),
lastRefreshed: new Date(),
}
: {
_id: hash,
notFound: true,
};
// Upsert the map: if it exists, update it; if not, create a new one
map = await BeatSaverMapModel.findOneAndUpdate({ _id: hash }, newMapData, {
upsert: true,
new: true,
setDefaultsOnInsert: true,
});
if (map == null || map.notFound) {
return undefined;
}
return map.toObject() as BeatSaverMap;
}
}

View File

@ -0,0 +1,218 @@
import { ImageResponse } from "@vercel/og";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import React from "react";
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
import { StarIcon } from "../../components/star-icon";
import { GlobeIcon } from "../../components/globe-icon";
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
import { Jimp } from "jimp";
import { extractColors } from "extract-colors";
import { Config } from "@ssr/common/config";
import { fetchWithCache } from "../common/cache.util";
import { SSRCache } from "@ssr/common/cache";
import { getScoreSaberPlayerFromToken } from "@ssr/common/token-creators";
import LeaderboardService from "./leaderboard.service";
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
const cache = new SSRCache({
ttl: 1000 * 60 * 60, // 1 hour
});
const imageOptions = { width: 1200, height: 630 };
export class ImageService {
/**
* Gets the average color of an image
*
* @param src the image url
* @returns the average color
* @private
*/
public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> {
src = decodeURIComponent(src);
return await fetchWithCache<{ color: string }>(cache, `average_color-${src}`, async () => {
try {
const image = await Jimp.read(src); // Load image using Jimp
const { width, height, data } = image.bitmap; // Access image dimensions and pixel data
// Convert the Buffer data to Uint8ClampedArray
const uint8ClampedArray = new Uint8ClampedArray(data);
// Extract the colors using extract-colors
const colors = await extractColors({ data: uint8ClampedArray, width, height });
// Return the most dominant color, or fallback if none found
if (colors && colors.length > 0) {
return { color: colors[2].hex }; // Returning the third most dominant color
}
return {
color: "#fff", // Fallback color in case no colors are found
};
} catch (error) {
console.error("Error fetching image or extracting colors:", error);
return {
color: "#fff", // Fallback color in case of an error
};
}
});
}
/**
* The base of the OpenGraph image
*
* @param children the content of the image
*/
public static BaseImage({ children }: { children: React.ReactNode }) {
return (
<div
tw="w-full h-full flex flex-col text-white text-3xl p-3 justify-center items-center relative"
style={{
backgroundColor: "#0a0a0a",
background: "radial-gradient(ellipse 60% 60% at 50% -20%, rgba(120,119,198,0.15), rgba(255,255,255,0))",
}}
>
{children}
</div>
);
}
/**
* Renders the change for a stat.
*
* @param change the amount of change
* @param format the function to format the value
*/
private static renderDailyChange(change: number, format: (value: number) => string = formatNumberWithCommas) {
if (change === 0) {
return null;
}
return (
<p tw={`text-[23px] pl-1 m-0 ${change > 0 ? "text-green-400" : "text-red-400"}`}>
{change > 0 ? "+" : ""}
{format(change)}
</p>
);
}
/**
* Generates the OpenGraph image for the player
*
* @param id the player's id
*/
public static async generatePlayerImage(id: string) {
const player = await fetchWithCache<ScoreSaberPlayer>(cache, `player-${id}`, async () => {
const token = await scoresaberService.lookupPlayer(id);
return token ? await getScoreSaberPlayerFromToken(token, Config.apiUrl) : undefined;
});
if (!player) {
return undefined;
}
const { statisticChange } = player;
const { daily } = statisticChange ?? {};
const rankChange = daily?.countryRank ?? 0;
const countryRankChange = daily?.rank ?? 0;
const ppChange = daily?.pp ?? 0;
return new ImageResponse(
(
<ImageService.BaseImage>
{/* Player Avatar */}
<img src={player.avatar} width={256} height={256} alt="Player's Avatar" tw="rounded-full mb-3" />
{/* Player Stats */}
<div tw="flex flex-col pl-3 items-center">
{/* Player Name */}
<p tw="font-bold text-6xl m-0">{player.name}</p>
{/* Player PP */}
<div tw="flex justify-center items-center text-[33px]">
<p tw="text-[#4858ff] m-0">{formatPp(player.pp)}pp</p>
{this.renderDailyChange(ppChange)}
</div>
{/* Player Stats */}
<div tw="flex">
{/* Player Rank */}
<div tw="flex px-2 justify-center items-center">
<GlobeIcon />
<p tw="m-0">#{formatNumberWithCommas(player.rank)}</p>
{this.renderDailyChange(rankChange)}
</div>
{/* Player Country Rank */}
<div tw="flex px-2 justify-center items-center">
<img
src={`https://ssr.fascinated.cc/assets/flags/${player.country.toLowerCase()}.png`}
height={20}
alt="Player's Country"
/>
<p tw="pl-1 m-0">#{formatNumberWithCommas(player.countryRank)}</p>
{this.renderDailyChange(countryRankChange)}
</div>
</div>
{/* Joined Date */}
<p tw="m-0 text-gray-400 mt-2">
Joined ScoreSaber in{" "}
{player.joinedDate.toLocaleString("en-US", {
timeZone: "Europe/London",
month: "long",
year: "numeric",
})}
</p>
</div>
</ImageService.BaseImage>
),
imageOptions
);
}
/**
* Generates the OpenGraph image for the leaderboard
*
* @param id the leaderboard's id
*/
public static async generateLeaderboardImage(id: string) {
const response = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>("scoresaber", id);
if (!response) {
return undefined;
}
const { leaderboard } = response;
const ranked = leaderboard.stars > 0;
return new ImageResponse(
(
<ImageService.BaseImage>
{/* Leaderboard Cover Image */}
<img src={leaderboard.songArt} width={256} height={256} alt="Leaderboard Cover" tw="rounded-full mb-3" />
{/* Leaderboard Name */}
<p tw="font-bold text-6xl m-0">
{leaderboard.songName} {leaderboard.songSubName}
</p>
<div tw="flex justify-center items-center text-center">
{/* Leaderboard Stars */}
{ranked && (
<div tw="flex justify-center items-center text-4xl">
<p tw="font-bold m-0">{leaderboard.stars}</p>
<StarIcon />
</div>
)}
{/* Leaderboard Difficulty */}
<p tw={"font-bold m-0 text-4xl" + (ranked ? " pl-3" : "")}>{leaderboard.difficulty.difficulty}</p>
</div>
{/* Leaderboard Author */}
<p tw="font-bold text-2xl text-gray-400 m-0 mt-2">Mapped by {leaderboard.levelAuthorName}</p>
</ImageService.BaseImage>
),
imageOptions
);
}
}

View File

@ -0,0 +1,96 @@
import { Leaderboards } from "@ssr/common/leaderboard";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
import { NotFoundError } from "elysia";
import BeatSaverService from "./beatsaver.service";
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
import {
ScoreSaberLeaderboard,
ScoreSaberLeaderboardModel,
} from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
import Leaderboard from "@ssr/common/model/leaderboard/leaderboard";
export default class LeaderboardService {
/**
* Gets the leaderboard.
*
* @param leaderboard the leaderboard
* @param id the id
*/
private static async getLeaderboardToken<T>(leaderboard: Leaderboards, id: string): Promise<T | undefined> {
switch (leaderboard) {
case "scoresaber": {
return (await scoresaberService.lookupLeaderboard(id)) as T;
}
default: {
return undefined;
}
}
}
/**
* Gets a leaderboard.
*
* @param leaderboardName the leaderboard to get
* @param id the players id
* @returns the scores
*/
public static async getLeaderboard<L>(leaderboardName: Leaderboards, id: string): Promise<LeaderboardResponse<L>> {
let leaderboard: Leaderboard | undefined;
let beatSaverMap: BeatSaverMap | undefined;
const now = new Date();
switch (leaderboardName) {
case "scoresaber": {
let foundLeaderboard = false;
const cachedLeaderboard = await ScoreSaberLeaderboardModel.findById(id);
if (cachedLeaderboard != null) {
leaderboard = cachedLeaderboard.toObject() as unknown as ScoreSaberLeaderboard;
if (
leaderboard &&
(leaderboard.ranked || // Never refresh ranked leaderboards (it will get refreshed every night)
leaderboard.lastRefreshed == undefined || // Refresh if it has never been refreshed
now.getTime() - leaderboard.lastRefreshed.getTime() > 1000 * 60 * 60 * 24) // Refresh every day
) {
foundLeaderboard = true;
}
}
if (!foundLeaderboard) {
const leaderboardToken = await LeaderboardService.getLeaderboardToken<ScoreSaberLeaderboardToken>(
leaderboardName,
id
);
if (leaderboardToken == undefined) {
throw new NotFoundError(`Leaderboard not found for "${id}"`);
}
leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
leaderboard.lastRefreshed = new Date();
await ScoreSaberLeaderboardModel.findOneAndUpdate({ _id: id }, leaderboard, {
upsert: true,
new: true,
setDefaultsOnInsert: true,
});
}
if (leaderboard == undefined) {
throw new NotFoundError(`Leaderboard not found for "${id}"`);
}
beatSaverMap = await BeatSaverService.getMap(leaderboard.songHash);
break;
}
default: {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
}
return {
leaderboard: leaderboard as L,
beatsaver: beatSaverMap,
};
}
}

View File

@ -0,0 +1,371 @@
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
import { NotFoundError } from "@ssr/common/error/not-found-error";
import { getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-utils";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { InternalServerError } from "@ssr/common/error/internal-server-error";
import { delay, getPageFromRank, isProduction } from "@ssr/common/utils/utils";
import { AroundPlayer } from "@ssr/common/types/around-player";
import { ScoreSort } from "@ssr/common/score/score-sort";
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
import { ScoreService } from "./score.service";
import { logNewTrackedPlayer } from "../common/embds";
const SCORESABER_REQUEST_COOLDOWN = 60_000 / 250; // 250 requests per minute
const accountCreationLock: { [id: string]: Promise<PlayerDocument> } = {};
export class PlayerService {
public static async getPlayer(
id: string,
create: boolean = false,
playerToken?: ScoreSaberPlayerToken
): Promise<PlayerDocument> {
// Wait for the existing lock if it's in progress
if (accountCreationLock[id] !== undefined) {
await accountCreationLock[id];
}
let player: PlayerDocument | null = await PlayerModel.findById(id);
if (player === null) {
if (!create) {
throw new NotFoundError(`Player "${id}" not found`);
}
playerToken = playerToken || (await scoresaberService.lookupPlayer(id));
if (!playerToken) {
throw new NotFoundError(`Player "${id}" not found`);
}
// Create a new lock promise and assign it
accountCreationLock[id] = (async () => {
let newPlayer: PlayerDocument;
try {
console.log(`Creating player "${id}"...`);
newPlayer = (await PlayerModel.create({ _id: id })) as PlayerDocument;
newPlayer.trackedSince = new Date();
await newPlayer.save();
await this.seedPlayerHistory(newPlayer, playerToken);
await this.refreshAllPlayerScores(newPlayer);
// Notify in production
if (isProduction()) {
await logNewTrackedPlayer(playerToken);
}
} catch (err) {
console.log(`Failed to create player document for "${id}"`, err);
throw new InternalServerError(`Failed to create player document for "${id}"`);
} finally {
// Ensure the lock is always removed
delete accountCreationLock[id];
}
return newPlayer;
})();
// Wait for the player creation to complete
player = await accountCreationLock[id];
// Update player name
if (player.name !== playerToken.name) {
player.name = playerToken.name;
await player.save();
}
}
// Ensure that the player is now of type PlayerDocument
return player as PlayerDocument;
}
/**
* Seeds the player's history using data from
* the ScoreSaber API.
*
* @param player the player to seed
* @param playerToken the SoreSaber player token
*/
public static async seedPlayerHistory(player: PlayerDocument, playerToken: ScoreSaberPlayerToken): Promise<void> {
// Loop through rankHistory in reverse, from current day backwards
const playerRankHistory = playerToken.histories.split(",").map((value: string) => {
return parseInt(value);
});
playerRankHistory.push(playerToken.rank);
let daysAgo = 0; // Start from today
for (let i = playerRankHistory.length - daysAgo - 1; i >= 0; i--) {
const rank = playerRankHistory[i];
// Skip inactive days
if (rank == 999_999) {
continue;
}
const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo));
player.setStatisticHistory(date, {
rank: rank,
});
daysAgo += 1; // Increment daysAgo for each earlier rank
}
player.markModified("statisticHistory");
await player.save();
}
/**
* Tracks a players statistics
*
* @param foundPlayer the player to track
* @param playerToken an optional player token
*/
public static async trackScoreSaberPlayer(
foundPlayer: PlayerDocument,
playerToken?: ScoreSaberPlayerToken
): Promise<void> {
const dateToday = getMidnightAlignedDate(new Date());
const player = playerToken ? playerToken : await scoresaberService.lookupPlayer(foundPlayer.id);
if (player == undefined) {
console.log(`Player "${foundPlayer.id}" not found on ScoreSaber`);
return;
}
if (player.inactive) {
console.log(`Player "${foundPlayer.id}" is inactive on ScoreSaber`);
return;
}
// Seed the history with ScoreSaber data if no history exists
if (foundPlayer.getDaysTracked() === 0) {
await this.seedPlayerHistory(foundPlayer.id, player);
}
// Update current day's statistics
let history = foundPlayer.getHistoryByDate(dateToday);
if (history == undefined) {
history = {}; // Initialize if history is not found
}
const scoreStats = player.scoreStats;
// Set the history data
history.pp = player.pp;
history.countryRank = player.countryRank;
history.rank = player.rank;
history.accuracy = {
...history.accuracy,
averageRankedAccuracy: scoreStats.averageRankedAccuracy,
};
history.scores = {
rankedScores: 0,
unrankedScores: 0,
...history.scores,
totalScores: scoreStats.totalPlayCount,
totalRankedScores: scoreStats.rankedPlayCount,
};
history.score = {
...history.score,
totalScore: scoreStats.totalScore,
totalRankedScore: scoreStats.totalRankedScore,
};
foundPlayer.setStatisticHistory(dateToday, history);
foundPlayer.sortStatisticHistory();
foundPlayer.lastTracked = new Date();
foundPlayer.markModified("statisticHistory");
await foundPlayer.save();
console.log(`Tracked player "${foundPlayer.id}"!`);
}
/**
* Gets the players around a player.
*
* @param id the player to get around
* @param type the type to get around
*/
public static async getPlayersAroundPlayer(id: string, type: AroundPlayer): Promise<ScoreSaberPlayerToken[]> {
const getRank = (player: ScoreSaberPlayerToken, type: AroundPlayer) => {
switch (type) {
case "global":
return player.rank;
case "country":
return player.countryRank;
}
};
const itemsPerPage = 50;
const player = await scoresaberService.lookupPlayer(id);
if (player == undefined) {
throw new NotFoundError(`Player "${id}" not found`);
}
const rank = getRank(player, type);
const rankWithinPage = rank % itemsPerPage;
const pagesToSearch = [getPageFromRank(rank, itemsPerPage)];
if (rankWithinPage > 0) {
pagesToSearch.push(getPageFromRank(rank - 1, itemsPerPage));
} else if (rankWithinPage < itemsPerPage - 1) {
pagesToSearch.push(getPageFromRank(rank + 1, itemsPerPage));
}
const rankings: Map<string, ScoreSaberPlayerToken> = new Map();
for (const page of pagesToSearch) {
const response =
type == "global"
? await scoresaberService.lookupPlayers(page)
: await scoresaberService.lookupPlayersByCountry(page, player.country);
if (response == undefined) {
continue;
}
for (const player of response.players) {
if (rankings.has(player.id)) {
continue;
}
rankings.set(player.id, player);
}
}
const players = rankings
.values()
.toArray()
.sort((a, b) => {
return getRank(a, type) - getRank(b, type);
});
// Show 3 players above and 1 below the requested player
const playerPosition = players.findIndex(p => p.id === player.id);
const start = Math.max(0, playerPosition - 3);
let end = Math.min(players.length, playerPosition + 2);
const playersLength = players.slice(start, end).length;
// If there is less than 5 players to return, add more players to the end
if (playersLength < 5) {
end = Math.min(end + 5 - playersLength, players.length);
}
return players.slice(start, end);
}
/**
* Refreshes all the players scores.
*
* @param player the player to refresh
*/
public static async refreshAllPlayerScores(player: PlayerDocument) {
await this.refreshPlayerScoreSaberScores(player);
}
/**
* Ensures that all the players scores from the
* ScoreSaber API are up-to-date.
*
* @param player the player to refresh
* @private
*/
private static async refreshPlayerScoreSaberScores(player: PlayerDocument) {
console.log(`Refreshing scores for ${player.id}...`);
let page = 1;
let hasMorePages = true;
while (hasMorePages) {
const scoresPage = await scoresaberService.lookupPlayerScores({
playerId: player.id,
page: page,
limit: 100,
sort: ScoreSort.recent,
});
if (!scoresPage) {
console.warn(`Failed to fetch scores for ${player.id} on page ${page}.`);
break;
}
let missingScores = 0;
for (const score of scoresPage.playerScores) {
const leaderboard = getScoreSaberLeaderboardFromToken(score.leaderboard);
const scoreSaberScore = await ScoreService.getScoreSaberScore(
player.id,
leaderboard.id + "",
leaderboard.difficulty.difficulty,
leaderboard.difficulty.characteristic,
score.score.baseScore
);
if (scoreSaberScore == null) {
missingScores++;
}
await ScoreService.trackScoreSaberScore(score.score, score.leaderboard, player.id);
}
// Stop paginating if no scores are missing OR if player has seededScores marked true
if ((missingScores === 0 && player.seededScores) || page >= Math.ceil(scoresPage.metadata.total / 100)) {
hasMorePages = false;
}
page++;
await delay(SCORESABER_REQUEST_COOLDOWN); // Cooldown between page requests
}
// Mark player as seeded
player.seededScores = true;
await player.save();
console.log(`Finished refreshing scores for ${player.id}, total pages refreshed: ${page - 1}.`);
}
/**
* Ensures all player scores are up-to-date.
*/
public static async refreshPlayerScores() {
console.log(`Refreshing player score data...`);
const players = await PlayerModel.find({});
console.log(`Found ${players.length} players to refresh.`);
for (const player of players) {
await this.refreshAllPlayerScores(player);
await delay(SCORESABER_REQUEST_COOLDOWN); // Cooldown between players
}
}
/**
* Updates the player statistics for all players.
*/
public static async updatePlayerStatistics() {
const pages = 20; // top 1000 players
let toTrack: PlayerDocument[] = await PlayerModel.find({});
const toRemoveIds: string[] = [];
// loop through pages to fetch the top players
console.log(`Fetching ${pages} pages of players from ScoreSaber...`);
for (let i = 0; i < pages; i++) {
const pageNumber = i + 1;
console.log(`Fetching page ${pageNumber}...`);
const page = await scoresaberService.lookupPlayers(pageNumber);
if (page === undefined) {
console.log(`Failed to fetch players on page ${pageNumber}, skipping page...`);
await delay(SCORESABER_REQUEST_COOLDOWN);
continue;
}
for (const player of page.players) {
const foundPlayer = await PlayerService.getPlayer(player.id, true, player);
await PlayerService.trackScoreSaberPlayer(foundPlayer, player);
toRemoveIds.push(foundPlayer.id);
}
await delay(SCORESABER_REQUEST_COOLDOWN);
}
console.log(`Finished tracking player statistics for ${pages} pages, found ${toRemoveIds.length} players.`);
// remove all players that have been tracked
toTrack = toTrack.filter(player => !toRemoveIds.includes(player.id));
console.log(`Tracking ${toTrack.length} player statistics...`);
for (const player of toTrack) {
await PlayerService.trackScoreSaberPlayer(player);
await delay(SCORESABER_REQUEST_COOLDOWN);
}
console.log("Finished tracking player statistics.");
}
}

View File

@ -0,0 +1,714 @@
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token";
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
import { isProduction } from "@ssr/common/utils/utils";
import { Metadata } from "@ssr/common/types/metadata";
import { NotFoundError } from "elysia";
import BeatSaverService from "./beatsaver.service";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { ScoreSort } from "@ssr/common/score/score-sort";
import { Leaderboards } from "@ssr/common/leaderboard";
import LeaderboardService from "./leaderboard.service";
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
import { PlayerScore } from "@ssr/common/score/player-score";
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
import { DiscordChannels, logToChannel } from "../bot/bot";
import { EmbedBuilder } from "discord.js";
import { Config } from "@ssr/common/config";
import { SSRCache } from "@ssr/common/cache";
import { fetchWithCache } from "../common/cache.util";
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/score/score";
import {
AdditionalScoreData,
AdditionalScoreDataModel,
} from "@ssr/common/model/additional-score-data/additional-score-data";
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement";
import { ScoreType } from "@ssr/common/model/score/score";
import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators";
import {
ScoreSaberPreviousScore,
ScoreSaberScore,
ScoreSaberScoreModel,
} from "@ssr/common/model/score/impl/scoresaber-score";
import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token";
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
import { MapDifficulty } from "@ssr/common/score/map-difficulty";
import { MapCharacteristic } from "@ssr/common/types/map-characteristic";
import { Page, Pagination } from "@ssr/common/pagination";
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
import Leaderboard from "@ssr/common/model/leaderboard/leaderboard";
import { Timeframe } from "@ssr/common/timeframe";
import { getDaysAgoDate } from "@ssr/common/utils/time-utils";
import { PlayerService } from "./player.service";
const playerScoresCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
const leaderboardScoresCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
export class ScoreService {
/**
* Notifies the number one score in Discord.
*
* @param playerScore the score to notify
*/
public static async notifyNumberOne(playerScore: ScoreSaberPlayerScoreToken) {
// Only notify in production
if (!isProduction()) {
return;
}
const { score: scoreToken, leaderboard: leaderboardToken } = playerScore;
const leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
const score = getScoreSaberScoreFromToken(scoreToken, leaderboard, scoreToken.leaderboardPlayerInfo.id);
const playerInfo = score.playerInfo;
// Not ranked
if (leaderboard.stars <= 0) {
return;
}
// Not #1 rank
if (score.rank !== 1) {
return;
}
const player = await scoresaberService.lookupPlayer(playerInfo.id);
if (!player) {
return;
}
await logToChannel(
DiscordChannels.numberOneFeed,
new EmbedBuilder()
.setTitle(`${player.name} just set a #1!`)
.setDescription(
[
`${leaderboard.songName} ${leaderboard.songSubName} (${leaderboard.difficulty.difficulty} ${leaderboard.stars.toFixed(2)}★)`,
`[[Player]](${Config.websiteUrl}/player/${player.id}) [[Leaderboard]](${Config.websiteUrl}/leaderboard/${leaderboard.id})`,
].join("\n")
)
.addFields([
{
name: "Accuracy",
value: `${score.accuracy.toFixed(2)}%`,
inline: true,
},
{
name: "PP",
value: `${formatPp(score.pp)}pp`,
inline: true,
},
{
name: "Player Rank",
value: `#${formatNumberWithCommas(player.rank)}`,
inline: true,
},
{
name: "Misses",
value: formatNumberWithCommas(score.missedNotes),
inline: true,
},
{
name: "Bad Cuts",
value: formatNumberWithCommas(score.badCuts),
inline: true,
},
{
name: "Max Combo",
value: formatNumberWithCommas(score.maxCombo),
inline: true,
},
])
.setThumbnail(leaderboard.songArt)
.setTimestamp(score.timestamp)
.setColor("#00ff00")
);
}
/**
* Updates the players set scores count for today.
*
* @param score the score
*/
public static async updatePlayerScoresSet({
score: scoreToken,
leaderboard: leaderboardToken,
}: ScoreSaberPlayerScoreToken) {
const playerId = scoreToken.leaderboardPlayerInfo.id;
const leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
const player: PlayerDocument | null = await PlayerModel.findById(playerId);
// Player is not tracked, so ignore the score.
if (player == undefined) {
return;
}
const today = new Date();
const history = player.getHistoryByDate(today);
const scores = history.scores || {
rankedScores: 0,
unrankedScores: 0,
};
if (leaderboard.stars > 0) {
scores.rankedScores!++;
} else {
scores.unrankedScores!++;
}
history.scores = scores;
player.setStatisticHistory(today, history);
player.markModified("statisticHistory");
await player.save();
}
/**
* Tracks ScoreSaber score.
*
* @param scoreToken the score to track
* @param leaderboardToken the leaderboard for the score
* @param playerId the id of the player
*/
public static async trackScoreSaberScore(
scoreToken: ScoreSaberScoreToken,
leaderboardToken: ScoreSaberLeaderboardToken,
playerId?: string
) {
playerId = (scoreToken.leaderboardPlayerInfo && scoreToken.leaderboardPlayerInfo.id) || playerId;
if (!playerId) {
console.error(`Player ID is undefined, unable to track score: ${scoreToken.id}`);
return;
}
const playerName = (scoreToken.leaderboardPlayerInfo && scoreToken.leaderboardPlayerInfo.name) || "Unknown";
const leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
const score = getScoreSaberScoreFromToken(scoreToken, leaderboard, playerId);
const player: PlayerDocument | null = await PlayerModel.findById(playerId);
// Player is not tracked, so ignore the score.
if (player == undefined) {
return;
}
// Update player name
if (playerName !== "Unknown") {
player.name = playerName;
await player.save();
}
// The score has already been tracked, so ignore it.
if (
(await this.getScoreSaberScore(
playerId,
leaderboard.id + "",
leaderboard.difficulty.difficulty,
leaderboard.difficulty.characteristic,
score.score
)) !== null
) {
console.log(
`ScoreSaber score already tracked for "${playerName}"(${playerId}), difficulty: ${score.difficulty}, score: ${score.score}, leaderboard: ${leaderboard.id}, ignoring...`
);
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
delete score.playerInfo;
await ScoreSaberScoreModel.create(score);
console.log(
`Tracked ScoreSaber score for "${playerName}"(${playerId}), difficulty: ${score.difficulty}, score: ${score.score}, pp: ${score.pp.toFixed(2)}pp, leaderboard: ${leaderboard.id}`
);
}
/**
* Tracks BeatLeader score.
*
* @param score the score to track
*/
public static async trackBeatLeaderScore(score: BeatLeaderScoreToken) {
const { playerId, player: scorePlayer, leaderboard } = score;
const player: PlayerDocument | null = await PlayerModel.findById(playerId);
// Player is not tracked, so ignore the score.
if (player == undefined) {
return;
}
// The score has already been tracked, so ignore it.
if (
(await this.getAdditionalScoreData(
playerId,
leaderboard.song.hash,
leaderboard.difficulty.difficultyName,
score.baseScore
)) !== undefined
) {
return;
}
const getMisses = (score: BeatLeaderScoreToken | BeatLeaderScoreImprovementToken) => {
return score.missedNotes + score.badCuts + score.bombCuts;
};
const difficulty = leaderboard.difficulty;
const difficultyKey = `${difficulty.difficultyName}-${difficulty.modeName}`;
const rawScoreImprovement = score.scoreImprovement;
const data = {
playerId: playerId,
songHash: leaderboard.song.hash.toUpperCase(),
songDifficulty: difficultyKey,
songScore: score.baseScore,
scoreId: score.id,
leaderboardId: leaderboard.id,
misses: {
misses: getMisses(score),
missedNotes: score.missedNotes,
bombCuts: score.bombCuts,
badCuts: score.badCuts,
wallsHit: score.wallsHit,
},
pauses: score.pauses,
fcAccuracy: score.fcAccuracy * 100,
fullCombo: score.fullCombo,
handAccuracy: {
left: score.accLeft,
right: score.accRight,
},
timestamp: new Date(Number(score.timeset) * 1000),
} as AdditionalScoreData;
if (rawScoreImprovement && rawScoreImprovement.score > 0) {
data.scoreImprovement = {
score: rawScoreImprovement.score,
misses: {
misses: getMisses(rawScoreImprovement),
missedNotes: rawScoreImprovement.missedNotes,
bombCuts: rawScoreImprovement.bombCuts,
badCuts: rawScoreImprovement.badCuts,
wallsHit: rawScoreImprovement.wallsHit,
},
accuracy: rawScoreImprovement.accuracy * 100,
handAccuracy: {
left: rawScoreImprovement.accLeft,
right: rawScoreImprovement.accRight,
},
};
}
await AdditionalScoreDataModel.create(data);
console.log(
`Tracked additional score data for "${scorePlayer.name}"(${playerId}), difficulty: ${difficultyKey}, score: ${score.baseScore}`
);
}
/**
* Gets the top tracked scores.
*
* @param amount the amount of scores to get
* @param timeframe the timeframe to filter by
* @returns the top scores
*/
public static async getTopScores(amount: number = 100, timeframe: Timeframe) {
console.log(`Getting top scores for timeframe: ${timeframe}, limit: ${amount}...`);
const before = Date.now();
let daysAgo = -1;
if (timeframe === "daily") {
daysAgo = 1;
} else if (timeframe === "weekly") {
daysAgo = 8;
} else if (timeframe === "monthly") {
daysAgo = 31;
}
const date: Date = daysAgo == -1 ? new Date(0) : getDaysAgoDate(daysAgo);
const foundScores = await ScoreSaberScoreModel.aggregate([
{ $match: { timestamp: { $gte: date } } },
{
$group: {
_id: { leaderboardId: "$leaderboardId", playerId: "$playerId" },
score: { $first: "$$ROOT" },
},
},
{ $sort: { "score.pp": -1 } },
{ $limit: amount },
]);
const scores: PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>[] = [];
for (const { score: scoreData } of foundScores) {
const score = new ScoreSaberScoreModel(scoreData).toObject() as ScoreSaberScore;
const leaderboardResponse = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>(
"scoresaber",
score.leaderboardId + ""
);
if (!leaderboardResponse) {
continue;
}
const { leaderboard, beatsaver } = leaderboardResponse;
try {
const player = await PlayerService.getPlayer(score.playerId);
if (player !== undefined) {
score.playerInfo = {
id: player.id,
name: player.name,
};
}
} catch {
score.playerInfo = {
id: score.playerId,
};
}
const [additionalData, previousScore] = await Promise.all([
this.getAdditionalScoreData(
score.playerId,
leaderboard.songHash,
`${leaderboard.difficulty.difficulty}-${leaderboard.difficulty.characteristic}`,
score.score
),
this.getPreviousScore(score.playerId, leaderboard.id + "", score.timestamp),
]);
if (additionalData) {
score.additionalData = additionalData;
}
if (previousScore) {
score.previousScore = previousScore;
}
scores.push({
score: score,
leaderboard: leaderboard,
beatSaver: beatsaver,
});
}
console.log(`Got ${scores.length} scores in ${Date.now() - before}ms (timeframe: ${timeframe}, limit: ${amount})`);
return scores;
}
/**
* Gets the additional score data for a player's score.
*
* @param playerId the id of the player
* @param songHash the hash of the map
* @param songDifficulty the difficulty of the map
* @param songScore the score of the play
* @private
*/
private static async getAdditionalScoreData(
playerId: string,
songHash: string,
songDifficulty: string,
songScore: number
): Promise<AdditionalScoreData | undefined> {
const additionalData = await AdditionalScoreDataModel.findOne({
playerId: playerId,
songHash: songHash.toUpperCase(),
songDifficulty: songDifficulty,
songScore: songScore,
});
if (!additionalData) {
return undefined;
}
return additionalData.toObject();
}
/**
* Gets a ScoreSaber score.
*
* @param playerId the player who set the score
* @param leaderboardId the leaderboard id the score was set on
* @param difficulty the difficulty played
* @param characteristic the characteristic played
* @param score the score of the score set
*/
public static async getScoreSaberScore(
playerId: string,
leaderboardId: string,
difficulty: MapDifficulty,
characteristic: MapCharacteristic,
score: number
) {
return ScoreSaberScoreModel.findOne({
playerId: playerId,
leaderboardId: leaderboardId,
difficulty: difficulty,
characteristic: characteristic,
score: score,
});
}
public static async getPlayerScores(
leaderboardName: Leaderboards,
playerId: string,
page: number,
sort: string,
search?: string
): Promise<PlayerScoresResponse<unknown, unknown> | undefined> {
return fetchWithCache(
playerScoresCache,
`player-scores-${leaderboardName}-${playerId}-${page}-${sort}-${search}`,
async () => {
const scores: PlayerScore<unknown, unknown>[] = [];
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values
switch (leaderboardName) {
case "scoresaber": {
const leaderboardScores = await scoresaberService.lookupPlayerScores({
playerId,
page,
sort: sort as ScoreSort,
search,
});
if (leaderboardScores == undefined) {
break;
}
metadata = new Metadata(
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage),
leaderboardScores.metadata.total,
leaderboardScores.metadata.page,
leaderboardScores.metadata.itemsPerPage
);
const scorePromises = leaderboardScores.playerScores.map(async token => {
const leaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard);
if (!leaderboard) return undefined;
const score = getScoreSaberScoreFromToken(token.score, leaderboard, playerId);
if (!score) return undefined;
// Fetch additional data, previous score, and BeatSaver map concurrently
const [additionalData, previousScore, beatSaverMap] = await Promise.all([
this.getAdditionalScoreData(
playerId,
leaderboard.songHash,
`${leaderboard.difficulty.difficulty}-${leaderboard.difficulty.characteristic}`,
score.score
),
this.getPreviousScore(playerId, leaderboard.id + "", score.timestamp),
BeatSaverService.getMap(leaderboard.songHash),
]);
if (additionalData) {
score.additionalData = additionalData;
}
if (previousScore) {
score.previousScore = previousScore;
}
return {
score: score,
leaderboard: leaderboard,
beatSaver: beatSaverMap,
} as PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>;
});
const resolvedScores = (await Promise.all(scorePromises)).filter(
(s): s is PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard> => s !== undefined
);
scores.push(...resolvedScores);
break;
}
default: {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
}
return {
scores: scores,
metadata: metadata,
};
}
);
}
/**
* Gets scores for a leaderboard.
*
* @param leaderboardName the leaderboard to get the scores from
* @param leaderboardId the leaderboard id
* @param page the page to get
* @returns the scores
*/
public static async getLeaderboardScores(
leaderboardName: Leaderboards,
leaderboardId: string,
page: number
): Promise<LeaderboardScoresResponse<unknown, unknown> | undefined> {
return fetchWithCache(
leaderboardScoresCache,
`leaderboard-scores-${leaderboardName}-${leaderboardId}-${page}`,
async () => {
const scores: ScoreType[] = [];
let leaderboard: Leaderboard | undefined;
let beatSaverMap: BeatSaverMap | undefined;
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values
switch (leaderboardName) {
case "scoresaber": {
const leaderboardResponse = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>(
leaderboardName,
leaderboardId
);
if (leaderboardResponse == undefined) {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
leaderboard = leaderboardResponse.leaderboard;
beatSaverMap = leaderboardResponse.beatsaver;
const leaderboardScores = await scoresaberService.lookupLeaderboardScores(leaderboardId, page);
if (leaderboardScores == undefined) {
break;
}
for (const token of leaderboardScores.scores) {
const score = getScoreSaberScoreFromToken(
token,
leaderboardResponse.leaderboard,
token.leaderboardPlayerInfo.id
);
if (score == undefined) {
continue;
}
scores.push(score);
}
metadata = new Metadata(
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage),
leaderboardScores.metadata.total,
leaderboardScores.metadata.page,
leaderboardScores.metadata.itemsPerPage
);
break;
}
default: {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
}
return {
scores: scores,
leaderboard: leaderboard,
beatSaver: beatSaverMap,
metadata: metadata,
};
}
);
}
/**
* Gets the player's score history for a map.
*
* @param playerId the player's id to get the previous scores for
* @param leaderboardId the leaderboard to get the previous scores on
* @param page the page to get
*/
public static async getScoreHistory(
playerId: string,
leaderboardId: string,
page: number
): Promise<Page<PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>>> {
const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId })
.sort({ timestamp: -1 })
.skip(1);
if (scores == null || scores.length == 0) {
throw new NotFoundError(`No previous scores found for ${playerId} in ${leaderboardId}`);
}
return new Pagination<PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>>()
.setItemsPerPage(8)
.setTotalItems(scores.length)
.getPage(page, async () => {
const toReturn: PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>[] = [];
for (const score of scores) {
const leaderboardResponse = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>(
"scoresaber",
leaderboardId
);
if (leaderboardResponse == undefined) {
throw new NotFoundError(`Leaderboard "${leaderboardId}" not found`);
}
const { leaderboard, beatsaver } = leaderboardResponse;
const additionalData = await this.getAdditionalScoreData(
playerId,
leaderboard.songHash,
`${leaderboard.difficulty.difficulty}-${leaderboard.difficulty.characteristic}`,
score.score
);
if (additionalData !== undefined) {
score.additionalData = additionalData;
}
const previousScore = await this.getPreviousScore(playerId, leaderboardId, score.timestamp);
if (previousScore !== undefined) {
score.previousScore = previousScore;
}
toReturn.push({
score: score as unknown as ScoreSaberScore,
leaderboard: leaderboard,
beatSaver: beatsaver,
});
}
return toReturn;
});
}
/**
* Gets the player's previous score for a map.
*
* @param playerId the player's id to get the previous score for
* @param leaderboardId the leaderboard to get the previous score on
* @param timestamp the score's timestamp to get the previous score for
* @returns the score, or undefined if none
*/
public static async getPreviousScore(
playerId: string,
leaderboardId: string,
timestamp: Date
): Promise<ScoreSaberPreviousScore | undefined> {
const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId }).sort({
timestamp: -1,
});
if (scores == null || scores.length == 0) {
return undefined;
}
const scoreIndex = scores.findIndex(score => score.timestamp.getTime() == timestamp.getTime());
const score = scores.find(score => score.timestamp.getTime() == timestamp.getTime());
if (scoreIndex == -1 || score == undefined) {
return undefined;
}
const previousScore = scores[scoreIndex + 1];
if (previousScore == undefined) {
return undefined;
}
return {
score: previousScore.score,
accuracy: previousScore.accuracy,
modifiers: previousScore.modifiers,
misses: previousScore.misses,
missedNotes: previousScore.missedNotes,
badCuts: previousScore.badCuts,
fullCombo: previousScore.fullCombo,
pp: previousScore.pp,
weight: previousScore.weight,
maxCombo: previousScore.maxCombo,
timestamp: previousScore.timestamp,
change: {
score: score.score - previousScore.score,
accuracy: score.accuracy - previousScore.accuracy,
misses: score.misses - previousScore.misses,
missedNotes: score.missedNotes - previousScore.missedNotes,
badCuts: score.badCuts - previousScore.badCuts,
pp: score.pp - previousScore.pp,
weight: score.weight && previousScore.weight && score.weight - previousScore.weight,
maxCombo: score.maxCombo - previousScore.maxCombo,
},
} as ScoreSaberPreviousScore;
}
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"types": ["bun-types"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"jsx": "react",
"incremental": true
}
}

View File

@ -0,0 +1,27 @@
{
"name": "@ssr/common",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsc --watch --preserveWatchOutput",
"build": "tsc"
},
"exports": {
"./*": {
"types": "./dist/*.d.ts",
"import": "./dist/*.js",
"require": "./dist/*.js",
"default": "./dist/*.js"
}
},
"devDependencies": {
"@types/node": "^22.7.4",
"typescript": "^5"
},
"dependencies": {
"@typegoose/auto-increment": "^4.7.0",
"@typegoose/typegoose": "^12.8.0",
"ky": "^1.7.2",
"ws": "^8.18.0"
}
}

View File

@ -0,0 +1,129 @@
type CacheOptions = {
/**
* The time (in ms) the cached object will be valid for
*/
ttl?: number;
/**
* How often to check for expired objects
*/
checkInterval?: number;
/**
* Enable debug messages
*/
debug?: boolean;
};
type CachedObject = {
/**
* The cached object
*/
value: any;
/**
* The timestamp the object was cached
*/
timestamp: number;
};
export class SSRCache {
/**
* The time the cached object will be valid for
* @private
*/
private readonly ttl: number | undefined;
/**
* How often to check for expired objects
* @private
*/
private readonly checkInterval: number | undefined;
/**
* Enable debug messages
* @private
*/
private readonly debug: boolean;
/**
* The objects that have been cached
* @private
*/
private cache = new Map<string, CachedObject>();
constructor({ ttl, checkInterval, debug }: CacheOptions) {
this.ttl = ttl;
this.checkInterval = checkInterval || this.ttl ? 1000 * 60 : undefined; // 1 minute
this.debug = debug || false;
if (this.ttl !== undefined && this.checkInterval !== undefined) {
setInterval(() => {
for (const [key, value] of this.cache.entries()) {
if (value.timestamp + this.ttl! > Date.now()) {
continue;
}
this.remove(key);
}
}, this.checkInterval);
}
}
/**
* Gets an object from the cache
*
* @param key the cache key for the object
*/
public get<T>(key: string): T | undefined {
const cachedObject = this.cache.get(key);
if (cachedObject === undefined) {
if (this.debug) {
console.log(`Cache miss for key: ${key}`);
}
return undefined;
}
if (this.debug) {
console.log(`Retrieved ${key} from cache, value: ${JSON.stringify(cachedObject)}`);
}
return cachedObject.value as T;
}
/**
* Sets an object in the cache
*
* @param key the cache key
* @param value the object
*/
public set<T>(key: string, value: T): void {
this.cache.set(key, {
value,
timestamp: Date.now(),
});
if (this.debug) {
console.log(`Inserted ${key} into cache, value: ${JSON.stringify(value)}`);
}
}
/**
* Checks if an object is in the cache
*
* @param key the cache key
*/
public has(key: string): boolean {
return this.cache.has(key);
}
/**
* Removes an object from the cache
*
* @param key the cache key
*/
public remove(key: string): void {
this.cache.delete(key);
if (this.debug) {
console.log(`Removed ${key} from cache`);
}
}
}

View File

@ -0,0 +1,13 @@
export const Config = {
/**
* All projects
*/
websiteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc",
apiUrl: process.env.NEXT_PUBLIC_SITE_API || "https://ssr.fascinated.cc/api",
/**
* Backend
*/
mongoUri: process.env.MONGO_URI,
discordBotToken: process.env.DISCORD_BOT_TOKEN,
} as const;

View File

@ -0,0 +1,14 @@
export class CurvePoint {
constructor(
private acc: number,
private multiplier: number
) {}
getAcc(): number {
return this.acc;
}
getMultiplier(): number {
return this.multiplier;
}
}

View File

@ -0,0 +1,10 @@
import { HttpCode } from "../http-codes";
export class InternalServerError extends Error {
constructor(
public message: string = "internal-server-error",
public status: number = HttpCode.INTERNAL_SERVER_ERROR.code
) {
super(message);
}
}

View File

@ -0,0 +1,10 @@
import { HttpCode } from "../http-codes";
export class NotFoundError extends Error {
constructor(
public message: string = "not-found",
public status: number = HttpCode.NOT_FOUND.code
) {
super(message);
}
}

View File

@ -0,0 +1,10 @@
import { HttpCode } from "../http-codes";
export class RateLimitError extends Error {
constructor(
public message: string = "rate-limited",
public status: number = HttpCode.TOO_MANY_REQUESTS.code
) {
super(message);
}
}

View File

@ -0,0 +1,75 @@
export const HttpCode = {
// 1xx Informational
CONTINUE: { code: 100, message: "Continue" },
SWITCHING_PROTOCOLS: { code: 101, message: "Switching Protocols" },
PROCESSING: { code: 102, message: "Processing" },
EARLY_HINTS: { code: 103, message: "Early Hints" },
// 2xx Success
OK: { code: 200, message: "OK" },
CREATED: { code: 201, message: "Created" },
ACCEPTED: { code: 202, message: "Accepted" },
NON_AUTHORITATIVE_INFORMATION: { code: 203, message: "Non-Authoritative Information" },
NO_CONTENT: { code: 204, message: "No Content" },
RESET_CONTENT: { code: 205, message: "Reset Content" },
PARTIAL_CONTENT: { code: 206, message: "Partial Content" },
MULTI_STATUS: { code: 207, message: "Multi-Status" },
ALREADY_REPORTED: { code: 208, message: "Already Reported" },
IM_USED: { code: 226, message: "IM Used" },
// 3xx Redirection
MULTIPLE_CHOICES: { code: 300, message: "Multiple Choices" },
MOVED_PERMANENTLY: { code: 301, message: "Moved Permanently" },
FOUND: { code: 302, message: "Found" },
SEE_OTHER: { code: 303, message: "See Other" },
NOT_MODIFIED: { code: 304, message: "Not Modified" },
USE_PROXY: { code: 305, message: "Use Proxy" },
TEMPORARY_REDIRECT: { code: 307, message: "Temporary Redirect" },
PERMANENT_REDIRECT: { code: 308, message: "Permanent Redirect" },
// 4xx Client Errors
BAD_REQUEST: { code: 400, message: "Bad Request" },
UNAUTHORIZED: { code: 401, message: "Unauthorized" },
PAYMENT_REQUIRED: { code: 402, message: "Payment Required" },
FORBIDDEN: { code: 403, message: "Forbidden" },
NOT_FOUND: { code: 404, message: "Not Found" },
METHOD_NOT_ALLOWED: { code: 405, message: "Method Not Allowed" },
NOT_ACCEPTABLE: { code: 406, message: "Not Acceptable" },
PROXY_AUTHENTICATION_REQUIRED: { code: 407, message: "Proxy Authentication Required" },
REQUEST_TIMEOUT: { code: 408, message: "Request Timeout" },
CONFLICT: { code: 409, message: "Conflict" },
GONE: { code: 410, message: "Gone" },
LENGTH_REQUIRED: { code: 411, message: "Length Required" },
PRECONDITION_FAILED: { code: 412, message: "Precondition Failed" },
PAYLOAD_TOO_LARGE: { code: 413, message: "Payload Too Large" },
URI_TOO_LONG: { code: 414, message: "URI Too Long" },
UNSUPPORTED_MEDIA_TYPE: { code: 415, message: "Unsupported Media Type" },
RANGE_NOT_SATISFIABLE: { code: 416, message: "Range Not Satisfiable" },
EXPECTATION_FAILED: { code: 417, message: "Expectation Failed" },
IM_A_TEAPOT: { code: 418, message: "I'm a teapot" },
MISDIRECTED_REQUEST: { code: 421, message: "Misdirected Request" },
UNPROCESSABLE_ENTITY: { code: 422, message: "Unprocessable Entity" },
LOCKED: { code: 423, message: "Locked" },
FAILED_DEPENDENCY: { code: 424, message: "Failed Dependency" },
TOO_EARLY: { code: 425, message: "Too Early" },
UPGRADE_REQUIRED: { code: 426, message: "Upgrade Required" },
PRECONDITION_REQUIRED: { code: 428, message: "Precondition Required" },
TOO_MANY_REQUESTS: { code: 429, message: "Too Many Requests" },
REQUEST_HEADER_FIELDS_TOO_LARGE: { code: 431, message: "Request Header Fields Too Large" },
UNAVAILABLE_FOR_LEGAL_REASONS: { code: 451, message: "Unavailable For Legal Reasons" },
// 5xx Server Errors
INTERNAL_SERVER_ERROR: { code: 500, message: "Internal Server Error" },
NOT_IMPLEMENTED: { code: 501, message: "Not Implemented" },
BAD_GATEWAY: { code: 502, message: "Bad Gateway" },
SERVICE_UNAVAILABLE: { code: 503, message: "Service Unavailable" },
GATEWAY_TIMEOUT: { code: 504, message: "Gateway Timeout" },
HTTP_VERSION_NOT_SUPPORTED: { code: 505, message: "HTTP Version Not Supported" },
VARIANT_ALSO_NEGOTIATES: { code: 506, message: "Variant Also Negotiates" },
INSUFFICIENT_STORAGE: { code: 507, message: "Insufficient Storage" },
LOOP_DETECTED: { code: 508, message: "Loop Detected" },
NOT_EXTENDED: { code: 510, message: "Not Extended" },
NETWORK_AUTHENTICATION_REQUIRED: { code: 511, message: "Network Authentication Required" },
} as const;
export type HttpCode = typeof HttpCode[keyof typeof HttpCode];

View File

@ -0,0 +1,5 @@
const Leaderboards = {
SCORESABER: "scoresaber",
} as const;
export type Leaderboards = (typeof Leaderboards)[keyof typeof Leaderboards];

View File

@ -0,0 +1,133 @@
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
import { HandAccuracy } from "./hand-accuracy";
import { Misses } from "./misses";
/**
* The model for additional score data.
*/
@modelOptions({
options: { allowMixed: Severity.ALLOW },
schemaOptions: {
collection: "additional-score-data",
toObject: {
virtuals: true,
transform: function (_, ret) {
delete ret._id;
delete ret.playerId;
delete ret.songHash;
delete ret.songDifficulty;
delete ret.songScore;
delete ret.__v;
return ret;
},
},
},
})
export class AdditionalScoreData {
/**
* The of the player who set the score.
*/
@prop({ required: true, index: true })
public playerId!: string;
/**
* The hash of the song.
*/
@prop({ required: true, index: true })
public songHash!: string;
/**
* The difficulty the score was set on.
*/
@prop({ required: true, index: true })
public songDifficulty!: string;
/**
* The score of the play.
*/
@prop({ required: true, index: true })
public songScore!: number;
// Above data is only so we can fetch it
// --------------------------------
/**
* The BeatLeader score id for this score.
*/
@prop({ required: false })
public scoreId!: number;
/**
* The BeatLeader leaderboard id for this score.
*/
@prop({ required: false })
public leaderboardId!: string;
/**
* The amount of pauses in the play.
*/
@prop({ required: false })
public pauses!: number;
/**
* The miss data for the play.
*/
@prop({ required: false, _id: false })
public misses!: Misses;
/**
* The hand accuracy for each hand.
* @private
*/
@prop({ required: false, _id: false })
public handAccuracy!: HandAccuracy;
/**
* The full combo accuracy of the play.
*/
@prop({ required: true })
public fcAccuracy!: number;
/**
* Whether the play was a full combo.
*/
@prop({ required: true })
public fullCombo!: boolean;
/**
* The score improvement.
*/
@prop({ required: false, _id: false })
public scoreImprovement?: {
/**
* The change in the score.
*/
score: number;
/**
* The change in the accuracy.
*/
accuracy: number;
/**
* The change in the misses.
*/
misses: Misses;
/**
* The change in the hand accuracy.
*/
handAccuracy: HandAccuracy;
};
/**
* The date the score was set on.
*/
@prop({ required: true, index: true })
public timestamp!: Date;
}
export type AdditionalScoreDataDocument = AdditionalScoreData & Document;
export const AdditionalScoreDataModel: ReturnModelType<typeof AdditionalScoreData> =
getModelForClass(AdditionalScoreData);

View File

@ -0,0 +1,15 @@
import { prop } from "@typegoose/typegoose";
export class HandAccuracy {
/**
* The left hand accuracy.
*/
@prop({ required: true })
left!: number;
/**
* The right hand accuracy.
*/
@prop({ required: true })
right!: number;
}

View File

@ -0,0 +1,33 @@
import { prop } from "@typegoose/typegoose";
export class Misses {
/**
* The amount of misses notes + bad cuts.
*/
@prop({ required: true })
misses!: number;
/**
* The total amount of notes that were missed.
*/
@prop({ required: true })
missedNotes!: number;
/**
* The amount of times a bomb was hit.
*/
@prop({ required: true })
bombCuts!: number;
/**
* The amount of walls hit in the play.
*/
@prop({ required: true })
wallsHit!: number;
/**
* The number of bad cuts.
*/
@prop({ required: true })
badCuts!: number;
}

View File

@ -0,0 +1,27 @@
import { prop } from "@typegoose/typegoose";
export default class BeatSaverAuthor {
/**
* The id of the author.
*/
@prop({ required: true })
id: number;
/**
* The name of the mapper.
*/
@prop({ required: true })
name: string;
/**
* The avatar URL for the mapper.
*/
@prop({ required: true })
avatar: string;
constructor(id: number, name: string, avatar: string) {
this.id = id;
this.name = name;
this.avatar = avatar;
}
}

View File

@ -0,0 +1,128 @@
import { prop } from "@typegoose/typegoose";
import { type MapDifficulty } from "../../score/map-difficulty";
export default class BeatSaverMapDifficulty {
/**
* The NJS of this difficulty.
*/
@prop({ required: true })
njs: number;
/**
* The NJS offset of this difficulty.
*/
@prop({ required: true })
offset: number;
/**
* The amount of notes in this difficulty.
*/
@prop({ required: true })
notes: number;
/**
* The amount of bombs in this difficulty.
*/
@prop({ required: true })
bombs: number;
/**
* The amount of obstacles in this difficulty.
*/
@prop({ required: true })
obstacles: number;
/**
* The notes per second in this difficulty.
*/
@prop({ required: true })
nps: number;
/**
* The characteristic of this difficulty.
*/
@prop({ required: true, enum: ["Standard", "Lawless"] })
characteristic: "Standard" | "Lawless";
/**
* The difficulty level.
*/
@prop({ required: true })
difficulty: MapDifficulty;
/**
* The amount of lighting events in this difficulty.
*/
@prop({ required: true })
events: number;
/**
* Whether this difficulty uses Chroma.
*/
@prop({ required: true, default: false })
chroma: boolean;
/**
* Does this difficulty use Mapping Extensions.
*/
@prop({ required: true, default: false })
mappingExtensions: boolean;
/**
* Does this difficulty use Noodle Extensions.
*/
@prop({ required: true, default: false })
noodleExtensions: boolean;
/**
* Whether this difficulty uses cinema mode.
*/
@prop({ required: true, default: false })
cinema: boolean;
/**
* The maximum score achievable in this difficulty.
*/
@prop({ required: true })
maxScore: number;
/**
* The custom label for this difficulty.
*/
@prop()
label: string;
constructor(
njs: number,
offset: number,
notes: number,
bombs: number,
obstacles: number,
nps: number,
characteristic: "Standard" | "Lawless",
difficulty: MapDifficulty,
events: number,
chroma: boolean,
mappingExtensions: boolean,
noodleExtensions: boolean,
cinema: boolean,
maxScore: number,
label: string
) {
this.njs = njs;
this.offset = offset;
this.notes = notes;
this.bombs = bombs;
this.obstacles = obstacles;
this.nps = nps;
this.characteristic = characteristic;
this.difficulty = difficulty;
this.events = events;
this.chroma = chroma;
this.mappingExtensions = mappingExtensions;
this.noodleExtensions = noodleExtensions;
this.cinema = cinema;
this.maxScore = maxScore;
this.label = label;
}
}

View File

@ -0,0 +1,55 @@
import { prop } from "@typegoose/typegoose";
export default class BeatSaverMapMetadata {
/**
* The bpm of the song.
*/
@prop({ required: true })
bpm: number;
/**
* The song's length in seconds.
*/
@prop({ required: true })
duration: number;
/**
* The song's name.
*/
@prop({ required: true })
songName: string;
/**
* The song's sub name.
*/
@prop({ required: false })
songSubName: string;
/**
* The artist(s) name.
*/
@prop({ required: true })
songAuthorName: string;
/**
* The level mapper(s) name.
*/
@prop({ required: true })
levelAuthorName: string;
constructor(
bpm: number,
duration: number,
songName: string,
songSubName: string,
songAuthorName: string,
levelAuthorName: string
) {
this.bpm = bpm;
this.duration = duration;
this.songName = songName;
this.songSubName = songSubName;
this.songAuthorName = songAuthorName;
this.levelAuthorName = levelAuthorName;
}
}

View File

@ -0,0 +1,31 @@
import { modelOptions, prop, Severity } from "@typegoose/typegoose";
import BeatSaverMapDifficulty from "./map-difficulty";
@modelOptions({
options: { allowMixed: Severity.ALLOW },
})
export default class BeatSaverMapVersion {
/**
* The hash of this map.
*/
@prop({ required: true })
hash: string;
/**
* The date the map was created.
*/
@prop({ required: true })
createdAt: Date;
/**
* The difficulties of this map.
*/
@prop({ required: true })
difficulties: BeatSaverMapDifficulty[];
constructor(hash: string, createdAt: Date, difficulties: BeatSaverMapDifficulty[]) {
this.hash = hash;
this.createdAt = createdAt;
this.difficulties = difficulties;
}
}

View File

@ -0,0 +1,99 @@
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
import BeatSaverAuthor from "./author";
import BeatSaverMapVersion from "./map-version";
import BeatSaverMapMetadata from "./map-metadata";
/**
* The model for a BeatSaver map.
*/
@modelOptions({
options: { allowMixed: Severity.ALLOW },
schemaOptions: {
collection: "beatsaver-maps",
toObject: {
virtuals: true,
transform: function (_, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
},
},
},
})
export class BeatSaverMap {
/**
* The internal MongoDB ID (_id).
*/
@prop({ required: true })
private _id!: string;
/**
* The name of the map.
*/
@prop({ required: false })
public name!: string;
/**
* The description of the map.
*/
@prop({ required: false })
public description!: string;
/**
* The bsr code for the map.
*/
@prop({ required: false })
public bsr!: string;
/**
* The author of the map.
*/
@prop({ required: false, _id: false, type: () => BeatSaverAuthor })
public author!: BeatSaverAuthor;
/**
* The versions of the map.
*/
@prop({ required: false, _id: false, type: () => [BeatSaverMapVersion] })
public versions!: BeatSaverMapVersion[];
/**
* The metadata of the map.
*/
@prop({ required: false, _id: false, type: () => BeatSaverMapMetadata })
public metadata!: BeatSaverMapMetadata;
/**
* True if the map is not found on beatsaver.
*/
@prop({ required: false })
public notFound?: boolean;
/**
* The last time the map data was refreshed.
*/
@prop({ required: true })
public lastRefreshed!: Date;
/**
* Exposes `id` as a virtual field mapped from `_id`.
*/
public get id(): string {
return this._id;
}
/**
* Should the map data be refreshed?
*
* @returns true if the map data should be refreshed
*/
public shouldRefresh(): boolean {
const now = new Date();
return now.getTime() - this.lastRefreshed.getTime() > 1000 * 60 * 60 * 24 * 3; // 3 days
}
}
export type BeatSaverMapDocument = BeatSaverMap & Document;
export const BeatSaverMapModel: ReturnModelType<typeof BeatSaverMap> = getModelForClass(BeatSaverMap);

View File

@ -0,0 +1,56 @@
import Leaderboard from "../leaderboard";
import { type LeaderboardStatus } from "../leaderboard-status";
import { getModelForClass, modelOptions, Prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
@modelOptions({
options: { allowMixed: Severity.ALLOW },
schemaOptions: {
collection: "scoresaber-leaderboards",
toObject: {
virtuals: true,
transform: function (_, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
},
},
},
})
export default class ScoreSaberLeaderboardInternal extends Leaderboard {
/**
* The star count for the leaderboard.
*/
@Prop({ required: true })
readonly stars!: number;
/**
* The total amount of plays.
*/
@Prop({ required: true })
readonly plays!: number;
/**
* The amount of plays today.
*/
@Prop({ required: true })
readonly dailyPlays!: number;
/**
* Whether this leaderboard is qualified to be ranked.
*/
@Prop({ required: true })
readonly qualified!: boolean;
/**
* The status of the map.
*/
@Prop({ required: true })
readonly status!: LeaderboardStatus;
}
export type ScoreSaberLeaderboard = InstanceType<typeof ScoreSaberLeaderboardInternal>;
export type ScoreSaberLeaderboardDocument = ScoreSaberLeaderboard & Document;
export const ScoreSaberLeaderboardModel: ReturnModelType<typeof ScoreSaberLeaderboardInternal> =
getModelForClass(ScoreSaberLeaderboardInternal);

View File

@ -0,0 +1,29 @@
import { type MapDifficulty } from "../../score/map-difficulty";
import { type MapCharacteristic } from "../../types/map-characteristic";
import { Prop } from "@typegoose/typegoose";
export default class LeaderboardDifficulty {
/**
* The id of the leaderboard.
*/
@Prop({ required: true })
leaderboardId!: number;
/**
* The difficulty of the leaderboard.
*/
@Prop({ required: true })
difficulty!: MapDifficulty;
/**
* The characteristic of the leaderboard.
*/
@Prop({ required: true })
characteristic!: MapCharacteristic;
/**
* The raw difficulty of the leaderboard.
*/
@Prop({ required: true })
difficultyRaw!: string;
}

View File

@ -0,0 +1,4 @@
/**
* The status of the leaderboard.
*/
export type LeaderboardStatus = "Unranked" | "Ranked" | "Qualified";

View File

@ -0,0 +1,99 @@
import LeaderboardDifficulty from "./leaderboard-difficulty";
import { Prop } from "@typegoose/typegoose";
export default class Leaderboard {
/**
* The id of the leaderboard.
* @private
*/
@Prop({ required: true })
private readonly _id?: number;
/**
* The hash of the song this leaderboard is for.
* @private
*/
@Prop({ required: true })
readonly songHash!: string;
/**
* The name of the song this leaderboard is for.
* @private
*/
@Prop({ required: true })
readonly songName!: string;
/**
* The sub name of the leaderboard.
* @private
*/
@Prop({ required: true })
readonly songSubName!: string;
/**
* The author of the song this leaderboard is for.
* @private
*/
@Prop({ required: true })
readonly songAuthorName!: string;
/**
* The author of the level this leaderboard is for.
* @private
*/
@Prop({ required: true })
readonly levelAuthorName!: string;
/**
* The difficulty of the leaderboard.
* @private
*/
@Prop({ required: true, _id: false, type: () => LeaderboardDifficulty })
readonly difficulty!: LeaderboardDifficulty;
/**
* The difficulties of the leaderboard.
* @private
*/
@Prop({ required: true, _id: false, type: () => [LeaderboardDifficulty] })
readonly difficulties!: LeaderboardDifficulty[];
/**
* The maximum score of the leaderboard.
* @private
*/
@Prop({ required: true })
readonly maxScore!: number;
/**
* Whether the leaderboard is ranked.
* @private
*/
@Prop({ required: true })
readonly ranked!: boolean;
/**
* The link to the song art.
* @private
*/
@Prop({ required: true })
readonly songArt!: string;
/**
* The date the leaderboard was created.
* @private
*/
@Prop({ required: true })
readonly timestamp!: Date;
/**
* The date the leaderboard was last refreshed.
* @private
*/
@Prop({ required: true })
lastRefreshed?: Date;
get id(): number {
return this._id ?? 0;
}
}

View File

@ -0,0 +1,125 @@
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
import { PlayerHistory } from "../player/player-history";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../utils/time-utils";
/**
* The model for a player.
*/
@modelOptions({ options: { allowMixed: Severity.ALLOW } })
export class Player {
/**
* The id of the player.
*/
@prop()
public _id!: string;
/**
* The player's name.
*/
@prop()
public name?: string;
/**
* The player's statistic history.
*/
@prop()
private statisticHistory?: Record<string, PlayerHistory>;
/**
* Whether the player has their scores seeded.
*/
@prop()
public seededScores?: boolean;
/**
* The date the player was last tracked.
*/
@prop()
public lastTracked?: Date;
/**
* The date the player was first tracked.
*/
@prop()
public trackedSince?: Date;
/**
* Gets the player's statistic history.
*/
public getStatisticHistory(): Record<string, PlayerHistory> {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
return this.statisticHistory;
}
/**
* Gets the player's history for a specific date.
*
* @param date the date to get the history for.
*/
public getHistoryByDate(date: Date): PlayerHistory {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
return this.getStatisticHistory()[formatDateMinimal(getMidnightAlignedDate(date))] || {};
}
/**
* Gets the player's history for the previous X days.
*
* @param days the number of days to get the history for.
*/
public getHistoryPreviousDays(days: number): Record<string, PlayerHistory> {
const statisticHistory = this.getStatisticHistory();
const history: Record<string, PlayerHistory> = {};
for (let i = 0; i <= days; i++) {
const date = formatDateMinimal(getMidnightAlignedDate(getDaysAgoDate(i)));
const playerHistory = statisticHistory[date];
if (playerHistory !== undefined && Object.keys(playerHistory).length > 0) {
history[date] = playerHistory;
}
}
return history;
}
/**
* Sets the player's statistic history.
*
* @param date the date to set it for.
* @param history the history to set.
*/
public setStatisticHistory(date: Date, history: PlayerHistory) {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
this.statisticHistory[formatDateMinimal(getMidnightAlignedDate(date))] = history;
}
/**
* Sorts the player's statistic history by
* date in descending order. (oldest to newest)
*/
public sortStatisticHistory() {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
this.statisticHistory = Object.fromEntries(
Object.entries(this.statisticHistory).sort((a, b) => new Date(b[0]).getTime() - new Date(a[0]).getTime())
);
}
/**
* Gets the number of days tracked.
*
* @returns the number of days tracked.
*/
public getDaysTracked(): number {
return Object.keys(this.getStatisticHistory()).length;
}
}
export type PlayerDocument = Player & Document;
export const PlayerModel: ReturnModelType<typeof Player> = getModelForClass(Player);

View File

@ -0,0 +1,101 @@
import { getModelForClass, modelOptions, plugin, Prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import Score from "../score";
import { type ScoreSaberLeaderboardPlayerInfoToken } from "../../../types/token/scoresaber/score-saber-leaderboard-player-info-token";
import { Document } from "mongoose";
import { AutoIncrementID } from "@typegoose/auto-increment";
import { PreviousScore } from "../previous-score";
@modelOptions({
options: { allowMixed: Severity.ALLOW },
schemaOptions: {
collection: "scoresaber-scores",
toObject: {
virtuals: true,
transform: function (_, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
},
},
},
})
@plugin(AutoIncrementID, {
field: "_id",
startAt: 1,
trackerModelName: "scores",
trackerCollection: "increments",
overwriteModelName: "scoresaber-scores",
})
export class ScoreSaberScoreInternal extends Score {
/**
* The score's id.
*/
@Prop({ required: true, index: true })
public readonly scoreId!: string;
/**
* The leaderboard the score was set on.
*/
@Prop({ required: true, index: true })
public readonly leaderboardId!: number;
/**
* The amount of pp for the score.
* @private
*/
@Prop({ required: true, index: true })
public pp!: number;
/**
* The weight of the score, or undefined if not ranked.
* @private
*/
@Prop()
public readonly weight?: number;
/**
* The max combo of the score.
*/
@Prop({ required: true })
public readonly maxCombo!: number;
/**
* The previous score, if any.
*/
public previousScore?: ScoreSaberPreviousScore;
}
class ScoreSaberScorePublic extends ScoreSaberScoreInternal {
/**
* The player who set the score.
*/
public playerInfo!: ScoreSaberLeaderboardPlayerInfoToken;
}
export type ScoreSaberPreviousScore = PreviousScore & {
/**
* The pp of the previous score.
*/
pp: number;
/**
* The weight of the previous score.
*/
weight: number;
/**
* The max combo of the previous score.
*/
maxCombo: number;
/**
* The change between the previous score and the current score.
*/
change?: ScoreSaberPreviousScore;
};
export type ScoreSaberScore = InstanceType<typeof ScoreSaberScorePublic>;
export type ScoreSaberScoreDocument = ScoreSaberScore & Document;
export const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> =
getModelForClass(ScoreSaberScoreInternal);

View File

@ -0,0 +1,43 @@
import { Modifier } from "../../score/modifier";
export type PreviousScore = {
/**
* The score of the previous score.
*/
score: number;
/**
* The accuracy of the previous score.
*/
accuracy: number;
/**
* The modifiers of the previous score.
*/
modifiers?: Modifier[];
/**
* The misses of the previous score.
*/
misses: number;
/**
* The missed notes of the previous score.
*/
missedNotes: number;
/**
* The bad cuts of the previous score.
*/
badCuts: number;
/**
* The full combo of the previous score.
*/
fullCombo?: boolean;
/**
* When the previous score was set.
*/
timestamp: Date;
};

View File

@ -0,0 +1,102 @@
import { Modifier } from "../../score/modifier";
import { AdditionalScoreData } from "../additional-score-data/additional-score-data";
import { prop } from "@typegoose/typegoose";
import { type MapDifficulty } from "../../score/map-difficulty";
import { type MapCharacteristic } from "../../types/map-characteristic";
/**
* The model for a score.
*/
export default class Score {
/**
* The internal score id.
*/
@prop()
public _id?: number;
/**
* The id of the player who set the score.
*/
@prop({ required: true, index: true })
public readonly playerId!: string;
/**
* The map difficulty played in the score.
*/
@prop({ required: true })
public readonly difficulty!: MapDifficulty;
/**
* The characteristic of the map.
*/
@prop({ required: true })
public readonly characteristic!: MapCharacteristic;
/**
* The base score for the score.
* @private
*/
@prop({ required: true })
public readonly score!: number;
/**
* The accuracy of the score.
*/
@prop({ required: true })
public readonly accuracy!: number;
/**
* The rank for the score.
* @private
*/
@prop({ required: true })
public readonly rank!: number;
/**
* The modifiers used on the score.
* @private
*/
@prop({ enum: () => Modifier, type: String, required: true })
public readonly modifiers!: Modifier[];
/**
* The total amount of misses.
* @private
*/
@prop({ required: true })
public readonly misses!: number;
/**
* The amount of missed notes.
*/
@prop({ required: true })
public readonly missedNotes!: number;
/**
* The amount of bad cuts.
* @private
*/
@prop({ required: true })
public readonly badCuts!: number;
/**
* Whether every note was hit.
* @private
*/
@prop({ required: true })
public readonly fullCombo!: boolean;
/**
* The additional data for the score.
*/
public additionalData?: AdditionalScoreData;
/**
* The time the score was set.
* @private
*/
@prop({ required: true, index: true })
public readonly timestamp!: Date;
}
export type ScoreType = InstanceType<typeof Score>;

View File

@ -0,0 +1,103 @@
import { Metadata } from "./types/metadata";
import { NotFoundError } from "./error/not-found-error";
type FetchItemsFunction<T> = (fetchItems: FetchItems) => Promise<T[]>;
export class Pagination<T> {
private itemsPerPage: number = 0;
private totalItems: number = 0;
private items: T[] | null = null; // Optional array to hold set items
/**
* Sets the number of items per page.
* @param itemsPerPage - The number of items per page.
* @returns the pagination
*/
setItemsPerPage(itemsPerPage: number): Pagination<T> {
this.itemsPerPage = itemsPerPage;
return this;
}
/**
* Sets the items to paginate.
* @param items - The items to paginate.
* @returns the pagination
*/
setItems(items: T[]): Pagination<T> {
this.items = items;
this.totalItems = items.length;
return this;
}
/**
* Sets the total number of items.
* @param totalItems - Total number of items.
* @returns the pagination
*/
setTotalItems(totalItems: number): Pagination<T> {
this.totalItems = totalItems;
return this;
}
/**
* Gets a page of items, using either static items or a dynamic fetchItems callback.
* @param page - The page number to retrieve.
* @param fetchItems - The async function to fetch items if setItems was not used.
* @returns A promise resolving to the page of items.
* @throws throws an error if the page number is invalid.
*/
async getPage(page: number, fetchItems?: FetchItemsFunction<T>): Promise<Page<T>> {
const totalPages = Math.ceil(this.totalItems / this.itemsPerPage);
if (page < 1 || page > totalPages) {
throw new NotFoundError("Invalid page number");
}
// Calculate the range of items to fetch for the current page
const start = (page - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
let pageItems: T[];
// Use set items if they are present, otherwise use fetchItems callback
if (this.items) {
pageItems = this.items.slice(start, end);
} else if (fetchItems) {
pageItems = await fetchItems(new FetchItems(start, end));
} else {
throw new Error("Items function is not set and no fetchItems callback provided");
}
return new Page<T>(pageItems, new Metadata(totalPages, this.totalItems, page, this.itemsPerPage));
}
}
class FetchItems {
readonly start: number;
readonly end: number;
constructor(start: number, end: number) {
this.start = start;
this.end = end;
}
}
export class Page<T> {
readonly items: T[];
readonly metadata: Metadata;
constructor(items: T[], metadata: Metadata) {
this.items = items;
this.metadata = metadata;
}
/**
* Converts the page to a JSON object.
*/
toJSON() {
return {
items: this.items,
metadata: this.metadata,
};
}
}

View File

@ -0,0 +1,145 @@
import Player, { StatisticChange } from "../player";
import { PlayerHistory } from "../player-history";
/**
* A ScoreSaber player.
*/
export default interface ScoreSaberPlayer extends Player {
/**
* The bio of the player.
*/
bio: ScoreSaberBio;
/**
* The amount of pp the player has.
*/
pp: number;
/**
* The change in pp compared to yesterday.
*/
statisticChange: StatisticChange | undefined;
/**
* The role the player has.
*/
role: string | undefined;
/**
* The badges the player has.
*/
badges: ScoreSaberBadge[];
/**
* The rank history for this player.
*/
statisticHistory: { [key: string]: PlayerHistory };
/**
* The statistics for this player.
*/
statistics: ScoreSaberPlayerStatistics;
/**
* The permissions the player has.
*/
permissions: number;
/**
* The pages for the players positions.
*/
rankPages: ScoreSaberRankPages;
/**
* Whether the player is banned or not.
*/
banned: boolean;
/**
* Whether the player is inactive or not.
*/
inactive: boolean;
/**
* Whether the player is having their
* statistics being tracked or not.
*/
isBeingTracked?: boolean;
}
/**
* A bio of a player.
*/
export type ScoreSaberBio = {
/**
* The lines of the bio including any html tags.
*/
lines: string[];
/**
* The lines of the bio stripped of all html tags.
*/
linesStripped: string[];
};
/**
* A badge for a player.
*/
export type ScoreSaberBadge = {
/**
* The URL to the badge.
*/
url: string;
/**
* The description of the badge.
*/
description: string;
};
/**
* The statistics for a player.
*/
export type ScoreSaberPlayerStatistics = {
/**
* The total amount of score accumulated over all scores.
*/
totalScore: number;
/**
* The total amount of ranked score accumulated over all scores.
*/
totalRankedScore: number;
/**
* The average ranked accuracy for all ranked scores.
*/
averageRankedAccuracy: number;
/**
* The total amount of scores set.
*/
totalPlayCount: number;
/**
* The total amount of ranked score set.
*/
rankedPlayCount: number;
/**
* The amount of times their replays were watched.
*/
replaysWatched: number;
};
export type ScoreSaberRankPages = {
/**
* Their page for their global rank position.
*/
global: number;
/**
* Their page for their country rank position.
*/
country: number;
};

View File

@ -0,0 +1,71 @@
export interface PlayerHistory {
/**
* The player's rank.
*/
rank?: number;
/**
* The player's country rank.
*/
countryRank?: number;
/**
* The pp of the player.
*/
pp?: number;
/**
* How many times replays of the player scores have been watched
*/
replaysWatched?: number;
/**
* The player's score stats.
*/
score?: {
/**
* The total amount of unranked and ranked score.
*/
totalScore?: number;
/**
* The total amount of ranked score.
*/
totalRankedScore?: number;
};
/**
* The player's scores stats.
*/
scores?: {
/**
* The amount of score set.
*/
rankedScores?: number;
/**
* The amount of unranked scores set.
*/
unrankedScores?: number;
/**
* The total amount of ranked scores
*/
totalRankedScores?: number;
/**
* The total amount of scores
*/
totalScores?: number;
};
/**
* The player's accuracy stats.
*/
accuracy?: {
/**
* The player's average ranked accuracy.
*/
averageRankedAccuracy?: number;
};
}

View File

@ -0,0 +1,64 @@
import ScoreSaberPlayer from "./impl/scoresaber-player";
import { StatisticRange } from "./player";
export type PlayerStatValue = {
/**
* The type of the stat.
*/
type: string;
/**
* The value of the stat.
*/
value: (player: ScoreSaberPlayer, range: StatisticRange) => number | undefined;
};
export type PlayerStatChangeType =
| "Rank"
| "CountryRank"
| "PerformancePoints"
| "TotalPlayCount"
| "RankedPlayCount"
| "TotalScore"
| "TotalRankedScore"
| "AverageRankedAccuracy"
| "TotalReplaysWatched";
export const PlayerStatChange: Record<PlayerStatChangeType, PlayerStatValue> = {
Rank: {
type: "Rank",
value: (player, range) => player.statisticChange?.[range].rank,
},
CountryRank: {
type: "Country Rank",
value: (player, range) => player.statisticChange?.[range].countryRank,
},
PerformancePoints: {
type: "Performance Points",
value: (player, range) => player.statisticChange?.[range].pp,
},
TotalPlayCount: {
type: "Total Play Count",
value: (player, range) => player.statisticChange?.[range].scores?.totalScores,
},
RankedPlayCount: {
type: "Ranked Play Count",
value: (player, range) => player.statisticChange?.[range].scores?.totalRankedScores,
},
TotalScore: {
type: "Total Score",
value: (player, range) => player.statisticChange?.[range].score?.totalScore,
},
TotalRankedScore: {
type: "Total Ranked Score",
value: (player, range) => player.statisticChange?.[range].score?.totalRankedScore,
},
AverageRankedAccuracy: {
type: "Average Ranked Accuracy",
value: (player, range) => player.statisticChange?.[range].accuracy?.averageRankedAccuracy,
},
TotalReplaysWatched: {
type: "Total Replays Watched",
value: (player, range) => player.statisticChange?.[range].replaysWatched,
},
};

View File

@ -4,11 +4,6 @@ export interface PlayerTrackedSince {
*/ */
tracked: boolean; tracked: boolean;
/**
* The date the player was first tracked
*/
trackedSince?: string;
/** /**
* The amount of days the player has been tracked * The amount of days the player has been tracked
*/ */

View File

@ -1,4 +1,4 @@
import { PlayerHistory } from "@/common/player/player-history"; import { PlayerHistory } from "./player-history";
export default class Player { export default class Player {
/** /**
@ -55,4 +55,7 @@ export default class Player {
} }
} }
export type StatisticChange = PlayerHistory; export type StatisticRange = "daily" | "weekly" | "monthly";
export type StatisticChange = {
[key in StatisticRange]: PlayerHistory;
};

View File

@ -0,0 +1,8 @@
import ScoreSaberPlayerToken from "../types/token/scoresaber/score-saber-player-token";
export type AroundPlayerResponse = {
/**
* The players around the player.
*/
players: ScoreSaberPlayerToken[];
};

View File

@ -0,0 +1,13 @@
import { BeatSaverMap } from "../model/beatsaver/map";
export type LeaderboardResponse<L> = {
/**
* The leaderboard.
*/
leaderboard: L;
/**
* The beatsaver map.
*/
beatsaver?: BeatSaverMap;
};

View File

@ -0,0 +1,24 @@
import { Metadata } from "../types/metadata";
import { BeatSaverMap } from "../model/beatsaver/map";
export default interface LeaderboardScoresResponse<S, L> {
/**
* The scores that were set.
*/
readonly scores: S[];
/**
* The leaderboard that was used.
*/
readonly leaderboard: L;
/**
* The beatsaver map for the song.
*/
readonly beatSaver?: BeatSaverMap;
/**
* The pagination metadata.
*/
readonly metadata: Metadata;
}

View File

@ -0,0 +1,14 @@
import { Metadata } from "../types/metadata";
import { PlayerScore } from "../score/player-score";
export default interface PlayerScoresResponse<S, L> {
/**
* The scores that were set.
*/
readonly scores: PlayerScore<S, L>[];
/**
* The pagination metadata.
*/
readonly metadata: Metadata;
}

View File

@ -0,0 +1,21 @@
import { ScoreSaberLeaderboard } from "src/model/leaderboard/impl/scoresaber-leaderboard";
import { ScoreSaberScore } from "../model/score/impl/scoresaber-score";
import { PlayerScore } from "../score/player-score";
import { Timeframe } from "../timeframe";
export type TopScoresResponse = {
/**
* The top scores.
*/
scores: PlayerScore<ScoreSaberScore, ScoreSaberLeaderboard>[];
/**
* The timeframe returned.
*/
timeframe: Timeframe;
/**
* The amount of scores to fetch.
*/
limit: number;
};

View File

@ -0,0 +1 @@
export type MapDifficulty = "Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus" | "Unknown";

View File

@ -2,17 +2,18 @@
* The score modifiers. * The score modifiers.
*/ */
export enum Modifier { export enum Modifier {
DA = "Disappearing Arrows", NF = "No Fail",
PM = "Pro Mode",
FS = "Faster Song", FS = "Faster Song",
SF = "Super Fast Song", SF = "Super Fast Song",
SS = "Slower Song", SS = "Slower Song",
GN = "Ghost Notes", GN = "Ghost Notes",
NA = "No Arrows", DA = "Disappearing Arrows",
NO = "No Obstacles",
SA = "Strict Angles", SA = "Strict Angles",
SC = "Small Notes", SC = "Small Notes",
PM = "Pro Mode",
CS = "Fail on Saber Clash",
IF = "One Life", IF = "One Life",
NO = "No Obstacles",
BE = "Battery Energy", BE = "Battery Energy",
NB = "No Bombs",
NA = "No Arrows",
} }

View File

@ -0,0 +1,6 @@
export default interface PlayerLeaderboardScore<S> {
/**
* The score that was set.
*/
readonly score: S;
}

View File

@ -0,0 +1,18 @@
import { BeatSaverMap } from "../model/beatsaver/map";
export interface PlayerScore<S, L> {
/**
* The score.
*/
readonly score: S;
/**
* The leaderboard the score was set on.
*/
readonly leaderboard: L;
/**
* The BeatSaver of the song.
*/
readonly beatSaver?: BeatSaverMap;
}

View File

@ -0,0 +1,34 @@
import Service from "../service";
import { ScoreStatsToken } from "../../types/token/beatleader/score-stats/score-stats";
const LOOKUP_MAP_STATS_BY_SCORE_ID_ENDPOINT = `https://cdn.scorestats.beatleader.xyz/:scoreId.json`;
class BeatLeaderService extends Service {
constructor() {
super("BeatLeader");
}
/**
* Looks up the score stats for a score
*
* @param scoreId the score id to get
* @returns the score stats, or undefined if nothing was found
*/
async lookupScoreStats(scoreId: number): Promise<ScoreStatsToken | undefined> {
const before = performance.now();
this.log(`Looking score stats for "${scoreId}"...`);
const response = await this.fetch<ScoreStatsToken>(
LOOKUP_MAP_STATS_BY_SCORE_ID_ENDPOINT.replace(":scoreId", scoreId + "")
);
// Score stats not found
if (response == undefined) {
return undefined;
}
this.log(`Found score stats for score "${scoreId}" in ${(performance.now() - before).toFixed(0)}ms`);
return response;
}
}
export const beatLeaderService = new BeatLeaderService();

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