434 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
cd09148acb Update dependency lucide-react to ^0.451.0 2024-10-09 01:05:35 +00:00
935d4d5589 Update dependency eslint-config-next to v14.2.15 2024-10-08 21:03:20 +00:00
528 changed files with 10395 additions and 2214 deletions

View File

@ -31,4 +31,9 @@ spec:
valueFrom:
secretKeyRef:
name: ssr-backend-secret
key: MONGO_URI
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
tls:
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

@ -7,7 +7,8 @@ metadata:
namespace: public-services
spec:
encryptedData:
MONGO_URI: AgCUTvItz+Z7ZwWJZuNmdYtNmXPLjxf+48wydwwewJ7AZshoVQSfTNTU+Cu09p6NC9EXCOwsPYQuAqor81puHh/LbYDhdFaGDXWXGg6R6ELPQkU4hDTvIiNyTdeNhn5tFNMdsqwkrsdxjrfMsH+wSHcxJpH1dTmhkEytdhElQmTVJxITtvQlxljR2i9vV87TqYp2ebaBqcj3bGX/pfGLqImOPFJMw8TZ8N1jOxEtLOS9RJJL2YnJP4WH4OQhC+FIb1gQ8N7SI2CMj5Mqt8sdxrGnPBznUGJDEED2Vv7uqRTt/4Kysfv9DN1FpyifiWjGx61tLjS9WfsBoF12JiUFEcpBhBsWHhPjBeyWd6N9LWflDCB47eTLQt/HbarRnjtFpD04jrhv7XXF8YzOtjQDhVqhRDUdybt7LjN4VGw8P4mXf7JinwV4G49orsb561yas5xSTtZgL0t0uI0OTH+AYsiyB8Q9jWlJmdKTCM0gFU19uV+fPKC6UWN9gHXX9p5KK7ZX10I2UnH3knqa/OCGFj96C6RGN4cQlErnhjCrmlyfdcFiET6vgIaYnCbv1IxwpzlooqtWfmXqpmFfshfPWPdGuEqGOBaPmFYNwXaXVKsigv8gKIbWTl+ZhEnAN51wDYVRGT36fh8KM98I1ZcaRJKYJcz8I7+MnmEUDG97boWDe0wi6dY+INnQErsxcJ9W37d3BmEB0o0PS0yKA4lJXXWByS6T8AUpUidHCgufglQIuDXo1nBqVPoSOQg0OAq/kn4zUvfbw2+l0aZKrm0UILd5ZwAuAciJ/x7XtvcGfrYEJUpN/1DC5qL4J8s=
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

View File

@ -1,4 +1,4 @@
name: "Deploy Backend"
name: Deploy Backend
on:
workflow_dispatch:
@ -8,57 +8,91 @@ on:
paths:
- projects/backend/**
- projects/common/**
- .gitea/kubernetes/backend/**
- .gitea/workflows/deploy-backend.yml
jobs:
deploy:
runs-on: ubuntu-latest
docker:
strategy:
matrix:
arch: ["ubuntu-latest"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps:
- name: Checkout code
# Checkout the repo
- name: Checkout
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:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
registry: git.fascinated.cc
fetch-depth: 0
- name: Build Image
uses: docker/build-push-action@v6
# Deploy to Dokku
- name: Push to dokku
uses: dokku/github-action@master
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 }}
git_remote_url: "ssh://dokku@51.158.63.74:22/ssr-backend"
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
- 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 }}
#name: "Deploy Backend"
#
#on:
# workflow_dispatch:
# push:
# branches:
# - master
# paths:
# - projects/backend/**
# - projects/common/**
# - .gitea/kubernetes/backend/**
# - .gitea/workflows/deploy-backend.yml
#
#jobs:
# deploy:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# 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:
# 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:
workflow_dispatch:
@ -8,56 +8,91 @@ on:
paths:
- projects/website/**
- projects/common/**
- .gitea/kubernetes/website/**
- .gitea/workflows/deploy-website.yml
- bun.lockb
jobs:
deploy:
runs-on: ubuntu-latest
docker:
strategy:
matrix:
arch: ["ubuntu-latest"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps:
- name: Checkout code
# Checkout the repo
- name: Checkout
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:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
registry: git.fascinated.cc
fetch-depth: 0
- name: Build Image
uses: docker/build-push-action@v6
# Deploy to Dokku
- name: Push to dokku
uses: dokku/github-action@master
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 }}
git_remote_url: "ssh://dokku@51.158.63.74:22/ssr-website"
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
- 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 }}
#name: "Deploy Website"
#
#on:
# workflow_dispatch:
# push:
# branches:
# - master
# paths:
# - projects/website/**
# - projects/common/**
# - .gitea/kubernetes/website/**
# - .gitea/workflows/deploy-website.yml
#
#jobs:
# deploy:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# 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:
# 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 }}

View File

@ -1,26 +0,0 @@
name: Fix bun lock file
on:
push:
paths:
- package.json
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Commit bun lock file
run: |
git config --global user.email "helper@fascinated.cc"
git config --global user.name "Bun Fixer"
git add bun.lockb
git commit -m "Fix bun lock file"
git push

View File

@ -4,4 +4,4 @@ This is the 3rd re-code of this project. The first one was a mess, the second on
## meow
meow
meow

BIN
bun.lockb

Binary file not shown.

View File

@ -13,6 +13,7 @@
"author": "fascinated7",
"license": "MIT",
"dependencies": {
"concurrently": "^9.0.1"
"concurrently": "^9.0.1",
"cross-env": "^7.0.3"
}
}

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": {
}
}

View File

@ -1,4 +1,4 @@
FROM oven/bun:1.1.30-alpine AS base
FROM oven/bun:1.1.33-alpine AS base
# Install dependencies
FROM base AS depends
@ -24,4 +24,11 @@ RUN bun --filter '@ssr/common' build
# Copy the backend project
COPY --from=depends /app/projects/backend ./projects/backend
CMD ["bun", "run", "--filter", "backend", "start"]
# 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,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

@ -3,7 +3,8 @@
"version": "1.0.0",
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts"
"start": "bun run src/index.ts",
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@bogeychan/elysia-etag": "^0.0.6",
@ -13,13 +14,23 @@
"@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",
"elysia-rate-limit": "^4.1.0",
"mongoose": "^8.7.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"

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

@ -1,9 +1,9 @@
/**
* Gets the app version.
*/
export function getAppVersion() {
export async function getAppVersion() {
if (!process.env.APP_VERSION) {
const packageJson = require("../../package.json");
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

@ -1,3 +0,0 @@
export const Config = {
mongoUri: process.env.MONGO_URI,
}

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

@ -1,13 +1,26 @@
import { Controller, Get } from "elysia-decorators";
import { getAppVersion } from "../common/app-utils";
import { getAppVersion } from "../common/app.util";
import { AppService } from "../service/app.service";
@Controller()
export default class AppController {
@Get("/")
public index() {
public async index() {
return {
app: "backend",
version: getAppVersion(),
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

@ -1,29 +1,38 @@
import { Controller, Get } from "elysia-decorators";
import { PlayerService } from "../service/player.service";
import { t } from "elysia";
import { PlayerHistory } from "@ssr/common/types/player/player-history";
import { PlayerTrackedSince } from "@ssr/common/types/player/player-tracked-since";
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", {
@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 },
params: { id, days },
query: { createIfMissing },
}: {
params: { id: string };
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(50) };
return { statistics: player.getHistoryPreviousDays(days) };
}
@Get("/tracked/:id", {
@ -51,4 +60,21 @@ export default class PlayerController {
};
}
}
@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

@ -3,28 +3,86 @@ import cors from "@elysiajs/cors";
import { decorators } from "elysia-decorators";
import { logger } from "@tqman/nice-logger";
import { swagger } from "@elysiajs/swagger";
import { rateLimit } from "elysia-rate-limit";
import { RateLimitError } from "./error/rate-limit-error";
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 { Config } from "./common/config";
import { setLogLevel } from "@typegoose/typegoose";
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: "success",
logLevel: (await Bun.file(".env").exists()) ? "success" : "warn",
path: ".env",
override: true,
});
// Connect to Mongo
await mongoose.connect(Config.mongoUri!); // Connect to MongoDB
setLogLevel("DEBUG");
// 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
@ -35,7 +93,7 @@ app.onError({ as: "global" }, ({ code, error }) => {
return error.all;
}
let status = "status" in error ? error.status : undefined;
const status = "status" in error ? error.status : undefined;
return {
...((status && { statusCode: status }) || { status: code }),
...(error.message != code && { message: error.message }),
@ -58,27 +116,11 @@ app.use(cors());
*/
app.use(
logger({
enabled: true,
mode: "combined",
})
);
/**
* Rate limit (100 requests per minute)
*/
app.use(
rateLimit({
scoping: "global",
duration: 60 * 1000,
max: 100,
skip: request => {
let [_, path] = request.url.split("/"); // Get the url parts
path === "" || (path === undefined && (path = "/")); // If we're on /, the path is undefined, so we set it to /
return path === "/"; // ignore all requests to /
},
errorResponse: new RateLimitError("Too many requests, please try again later"),
})
);
/**
* Security settings
*/
@ -95,22 +137,40 @@ app.use(
*/
app.use(
decorators({
controllers: [AppController, PlayerController],
controllers: [AppController, PlayerController, ImageController, ScoresController, LeaderboardController],
})
);
/**
* Swagger Documentation
*/
app.use(swagger());
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(() => {
app.onStart(async () => {
console.log("Listening on port http://localhost:8080");
if (isProduction()) {
await initDiscordBot();
}
});
/**
* Start cronjobs
*/
PlayerService.initCronjobs();
app.listen(8080);
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

@ -1,64 +1,82 @@
import { PlayerDocument, PlayerModel } from "../model/player";
import { NotFoundError } from "../error/not-found-error";
import { cron } from "@elysiajs/cron";
import { app } from "../index";
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 "../error/internal-server-error";
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 {
/**
* Initialize the cron jobs
*/
public static initCronjobs() {
app.use(
cron({
name: "player-statistics-tracker-cron",
pattern: "0 1 * * *", // Every day at 00:01 (midnight)
run: async () => {
const players: PlayerDocument[] = await PlayerModel.find({});
for (const player of players) {
await PlayerService.trackScoreSaberPlayer(getMidnightAlignedDate(new Date()), player);
}
},
})
);
}
/**
* Get a player from the database.
*
* @param id the player to fetch
* @param create if true, create the player if it doesn't exist
* @returns the player
* @throws NotFoundError if the player is not found
*/
public static async getPlayer(id: string, create: boolean = false): Promise<PlayerDocument> {
console.log(`Fetching player "${id}"...`);
let player: PlayerDocument | null = await PlayerModel.findById(id);
if (player === null && !create) {
console.log(`Player "${id}" not found.`);
throw new NotFoundError(`Player "${id}" not found`);
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) {
const playerToken = await scoresaberService.lookupPlayer(id);
if (playerToken === undefined) {
if (!create) {
throw new NotFoundError(`Player "${id}" not found`);
}
console.log(`Creating player "${id}"...`);
player = (await PlayerModel.create({ _id: id })) as any;
if (player === null) {
throw new InternalServerError(`Failed to create player document for "${id}"`);
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();
}
player.trackedSince = new Date();
await this.seedPlayerHistory(player, playerToken);
console.log(`Created player "${id}".`);
} else {
console.log(`Found player "${id}".`);
}
return player;
// Ensure that the player is now of type PlayerDocument
return player as PlayerDocument;
}
/**
@ -75,26 +93,36 @@ export class PlayerService {
});
playerRankHistory.push(playerToken.rank);
let daysAgo = 1; // Start from yesterday
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 dateToday the date to track
* @param foundPlayer the player to track
* @param playerToken an optional player token
*/
public static async trackScoreSaberPlayer(dateToday: Date, foundPlayer: PlayerDocument) {
const player = await scoresaberService.lookupPlayer(foundPlayer.id);
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;
@ -106,7 +134,7 @@ export class PlayerService {
// Seed the history with ScoreSaber data if no history exists
if (foundPlayer.getDaysTracked() === 0) {
await this.seedPlayerHistory(foundPlayer, player);
await this.seedPlayerHistory(foundPlayer.id, player);
}
// Update current day's statistics
@ -114,15 +142,230 @@ export class PlayerService {
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

@ -10,5 +10,7 @@
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"jsx": "react",
"incremental": true
}
}

View File

@ -19,6 +19,9 @@
"typescript": "^5"
},
"dependencies": {
"ky": "^1.7.2"
"@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

@ -1,4 +1,4 @@
import { HttpCode } from "../common/http-codes";
import { HttpCode } from "../http-codes";
export class InternalServerError extends Error {
constructor(

View File

@ -1,4 +1,4 @@
import { HttpCode } from "../common/http-codes";
import { HttpCode } from "../http-codes";
export class NotFoundError extends Error {
constructor(
@ -7,4 +7,4 @@ export class NotFoundError extends Error {
) {
super(message);
}
}
}

View File

@ -1,4 +1,4 @@
import { HttpCode } from "../common/http-codes";
import { HttpCode } from "../http-codes";
export class RateLimitError extends Error {
constructor(
@ -7,4 +7,4 @@ export class RateLimitError extends Error {
) {
super(message);
}
}
}

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

@ -1,7 +1,7 @@
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
import { PlayerHistory } from "@ssr/common/types/player/player-history";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-utils";
import { PlayerHistory } from "../player/player-history";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../utils/time-utils";
/**
* The model for a player.
@ -14,12 +14,24 @@ export class 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.
*/
@ -60,17 +72,15 @@ export class Player {
* @param days the number of days to get the history for.
*/
public getHistoryPreviousDays(days: number): Record<string, PlayerHistory> {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
const statisticHistory = this.getStatisticHistory();
const history: Record<string, PlayerHistory> = {};
for (let i = 0; i < days; i++) {
for (let i = 0; i <= days; i++) {
const date = formatDateMinimal(getMidnightAlignedDate(getDaysAgoDate(i)));
const playerHistory = this.getStatisticHistory()[date];
if (playerHistory === undefined || Object.keys(playerHistory).length === 0) {
continue;
const playerHistory = statisticHistory[date];
if (playerHistory !== undefined && Object.keys(playerHistory).length > 0) {
history[date] = playerHistory;
}
history[date] = playerHistory;
}
return history;
}
@ -85,7 +95,7 @@ export class Player {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
this.getStatisticHistory()[formatDateMinimal(getMidnightAlignedDate(date))] = history;
this.statisticHistory[formatDateMinimal(getMidnightAlignedDate(date))] = history;
}
/**
@ -96,9 +106,9 @@ export class Player {
if (this.statisticHistory === undefined) {
this.statisticHistory = {};
}
return Object.entries(this.getStatisticHistory())
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
this.statisticHistory = Object.fromEntries(
Object.entries(this.statisticHistory).sort((a, b) => new Date(b[0]).getTime() - new Date(a[0]).getTime())
);
}
/**
@ -111,8 +121,5 @@ export class Player {
}
}
// This type defines a Mongoose document based on Player.
export type PlayerDocument = Player & Document;
// This type ensures that PlayerModel returns Mongoose documents (PlayerDocument) that have Mongoose methods (save, remove, etc.)
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

@ -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.
*/
export enum Modifier {
DA = "Disappearing Arrows",
NF = "No Fail",
PM = "Pro Mode",
FS = "Faster Song",
SF = "Super Fast Song",
SS = "Slower Song",
GN = "Ghost Notes",
NA = "No Arrows",
NO = "No Obstacles",
DA = "Disappearing Arrows",
SA = "Strict Angles",
SC = "Small Notes",
PM = "Pro Mode",
CS = "Fail on Saber Clash",
IF = "One Life",
NO = "No Obstacles",
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();

View File

@ -1,5 +1,5 @@
import Service from "../service";
import { BeatSaverMapToken } from "../../types/token/beatsaver/beat-saver-map-token";
import { BeatSaverMapToken } from "../../types/token/beatsaver/map";
const API_BASE = "https://api.beatsaver.com";
const LOOKUP_MAP_BY_HASH_ENDPOINT = `${API_BASE}/maps/hash/:query`;
@ -13,7 +13,6 @@ class BeatSaverService extends Service {
* Gets the map that match the query.
*
* @param query the query to search for
* @param useProxy whether to use the proxy or not
* @returns the map that match the query, or undefined if no map were found
*/
async lookupMap(query: string): Promise<BeatSaverMapToken | undefined> {

View File

@ -2,10 +2,13 @@ import Service from "../service";
import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-token";
import ScoreSaberPlayerToken from "../../types/token/scoresaber/score-saber-player-token";
import { ScoreSaberPlayersPageToken } from "../../types/token/scoresaber/score-saber-players-page-token";
import { ScoreSort } from "../../types/score/score-sort";
import { ScoreSort } from "../../score/score-sort";
import ScoreSaberPlayerScoresPageToken from "../../types/token/scoresaber/score-saber-player-scores-page-token";
import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token";
import ScoreSaberLeaderboardScoresPageToken from "../../types/token/scoresaber/score-saber-leaderboard-scores-page-token";
import { clamp, lerp } from "../../utils/math-utils";
import { CurvePoint } from "../../curve-point";
import { SSRCache } from "../../cache";
const API_BASE = "https://scoresaber.com/api";
@ -24,7 +27,53 @@ const LOOKUP_PLAYER_SCORES_ENDPOINT = `${API_BASE}/player/:id/scores?limit=:limi
const LOOKUP_LEADERBOARD_ENDPOINT = `${API_BASE}/leaderboard/by-id/:id/info`;
const LOOKUP_LEADERBOARD_SCORES_ENDPOINT = `${API_BASE}/leaderboard/by-id/:id/scores?page=:page`;
const STAR_MULTIPLIER = 42.117208413;
const playerCache = new SSRCache({
ttl: 60, // 1 minute
});
class ScoreSaberService extends Service {
private curvePoints = [
new CurvePoint(0, 0),
new CurvePoint(0.6, 0.18223233667439062),
new CurvePoint(0.65, 0.5866010012767576),
new CurvePoint(0.7, 0.6125565959114954),
new CurvePoint(0.75, 0.6451808210101443),
new CurvePoint(0.8, 0.6872268862950283),
new CurvePoint(0.825, 0.7150465663454271),
new CurvePoint(0.85, 0.7462290664143185),
new CurvePoint(0.875, 0.7816934560296046),
new CurvePoint(0.9, 0.825756123560842),
new CurvePoint(0.91, 0.8488375988124467),
new CurvePoint(0.92, 0.8728710341448851),
new CurvePoint(0.93, 0.9039994071865736),
new CurvePoint(0.94, 0.9417362980580238),
new CurvePoint(0.95, 1),
new CurvePoint(0.955, 1.0388633331418984),
new CurvePoint(0.96, 1.0871883573850478),
new CurvePoint(0.965, 1.1552120359501035),
new CurvePoint(0.97, 1.2485807759957321),
new CurvePoint(0.9725, 1.3090333065057616),
new CurvePoint(0.975, 1.3807102743105126),
new CurvePoint(0.9775, 1.4664726399289512),
new CurvePoint(0.98, 1.5702410055532239),
new CurvePoint(0.9825, 1.697536248647543),
new CurvePoint(0.985, 1.8563887693647105),
new CurvePoint(0.9875, 2.058947159052738),
new CurvePoint(0.99, 2.324506282149922),
new CurvePoint(0.99125, 2.4902905794106913),
new CurvePoint(0.9925, 2.685667856592722),
new CurvePoint(0.99375, 2.9190155639254955),
new CurvePoint(0.995, 3.2022017597337955),
new CurvePoint(0.99625, 3.5526145337555373),
new CurvePoint(0.9975, 3.996793606763322),
new CurvePoint(0.99825, 4.325027383589547),
new CurvePoint(0.999, 4.715470646416203),
new CurvePoint(0.9995, 5.019543595874787),
new CurvePoint(1, 5.367394282890631),
];
constructor() {
super("ScoreSaber");
}
@ -35,7 +84,7 @@ class ScoreSaberService extends Service {
* @param query the query to search for
* @returns the players that match the query, or undefined if no players were found
*/
async searchPlayers(query: string): Promise<ScoreSaberPlayerSearchToken | undefined> {
public async searchPlayers(query: string): Promise<ScoreSaberPlayerSearchToken | undefined> {
const before = performance.now();
this.log(`Searching for players matching "${query}"...`);
const results = await this.fetch<ScoreSaberPlayerSearchToken>(SEARCH_PLAYERS_ENDPOINT.replace(":query", query));
@ -54,9 +103,13 @@ class ScoreSaberService extends Service {
* Looks up a player by their ID.
*
* @param playerId the ID of the player to look up
* @param cache whether to use the local cache
* @returns the player that matches the ID, or undefined
*/
async lookupPlayer(playerId: string): Promise<ScoreSaberPlayerToken | undefined> {
public async lookupPlayer(playerId: string, cache: boolean = false): Promise<ScoreSaberPlayerToken | undefined> {
if (cache && playerCache.has(playerId)) {
return playerCache.get(playerId);
}
const before = performance.now();
this.log(`Looking up player "${playerId}"...`);
const token = await this.fetch<ScoreSaberPlayerToken>(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId));
@ -64,6 +117,9 @@ class ScoreSaberService extends Service {
return undefined;
}
this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`);
if (cache) {
playerCache.set(playerId, token);
}
return token;
}
@ -73,7 +129,7 @@ class ScoreSaberService extends Service {
* @param page the page to get players for
* @returns the players on the page, or undefined
*/
async lookupPlayers(page: number): Promise<ScoreSaberPlayersPageToken | undefined> {
public async lookupPlayers(page: number): Promise<ScoreSaberPlayersPageToken | undefined> {
const before = performance.now();
this.log(`Looking up players on page "${page}"...`);
const response = await this.fetch<ScoreSaberPlayersPageToken>(
@ -93,7 +149,7 @@ class ScoreSaberService extends Service {
* @param country the country to get players for
* @returns the players on the page, or undefined
*/
async lookupPlayersByCountry(page: number, country: string): Promise<ScoreSaberPlayersPageToken | undefined> {
public async lookupPlayersByCountry(page: number, country: string): Promise<ScoreSaberPlayersPageToken | undefined> {
const before = performance.now();
this.log(`Looking up players on page "${page}" for country "${country}"...`);
const response = await this.fetch<ScoreSaberPlayersPageToken>(
@ -111,18 +167,21 @@ class ScoreSaberService extends Service {
*
* @param playerId the ID of the player to look up
* @param sort the sort to use
* @param limit the amount of sores to fetch
* @param page the page to get scores for
* @param search
* @param search the query to search for
* @returns the scores of the player, or undefined
*/
async lookupPlayerScores({
public async lookupPlayerScores({
playerId,
sort,
limit = 8,
page,
search,
}: {
playerId: string;
sort: ScoreSort;
limit?: number;
page: number;
search?: string;
useProxy?: boolean;
@ -133,7 +192,7 @@ class ScoreSaberService extends Service {
);
const response = await this.fetch<ScoreSaberPlayerScoresPageToken>(
LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId)
.replace(":limit", 8 + "")
.replace(":limit", limit + "")
.replace(":sort", sort)
.replace(":page", page + "") + (search ? `&search=${search}` : "")
);
@ -151,7 +210,7 @@ class ScoreSaberService extends Service {
*
* @param leaderboardId the ID of the leaderboard to look up
*/
async lookupLeaderboard(leaderboardId: string): Promise<ScoreSaberLeaderboardToken | undefined> {
public async lookupLeaderboard(leaderboardId: string): Promise<ScoreSaberLeaderboardToken | undefined> {
const before = performance.now();
this.log(`Looking up leaderboard "${leaderboardId}"...`);
const response = await this.fetch<ScoreSaberLeaderboardToken>(
@ -171,7 +230,7 @@ class ScoreSaberService extends Service {
* @param page the page to get scores for
* @returns the scores of the leaderboard, or undefined
*/
async lookupLeaderboardScores(
public async lookupLeaderboardScores(
leaderboardId: string,
page: number
): Promise<ScoreSaberLeaderboardScoresPageToken | undefined> {
@ -188,6 +247,53 @@ class ScoreSaberService extends Service {
);
return response;
}
/**
* Gets the modifier for the given accuracy.
*
* @param accuracy The accuracy.
* @return The modifier.
*/
public getModifier(accuracy: number): number {
accuracy = clamp(accuracy, 0, 100) / 100; // Normalize accuracy to a range of [0, 1]
if (accuracy <= 0) {
return 0;
}
if (accuracy >= 1) {
return this.curvePoints[this.curvePoints.length - 1].getMultiplier();
}
for (let i = 0; i < this.curvePoints.length - 1; i++) {
const point = this.curvePoints[i];
const nextPoint = this.curvePoints[i + 1];
if (accuracy >= point.getAcc() && accuracy <= nextPoint.getAcc()) {
return lerp(
point.getMultiplier(),
nextPoint.getMultiplier(),
(accuracy - point.getAcc()) / (nextPoint.getAcc() - point.getAcc())
);
}
}
return 0;
}
/**
* Gets the performance points (PP) based on stars and accuracy.
*
* @param stars The star count.
* @param accuracy The accuracy.
* @returns The calculated PP.
*/
public getPp(stars: number, accuracy: number): number {
if (accuracy <= 1) {
accuracy *= 100; // Convert the accuracy to a percentage
}
const pp = stars * STAR_MULTIPLIER; // Calculate base PP value
return this.getModifier(accuracy) * pp; // Calculate and return final PP value
}
}
export const scoresaberService = new ScoreSaberService();

View File

@ -1,4 +1,5 @@
import ky from "ky";
import { isServer } from "../utils/utils";
export default class Service {
/**
@ -38,9 +39,12 @@ export default class Service {
*/
public async fetch<T>(url: string): Promise<T | undefined> {
try {
return await ky.get<T>(this.buildRequestUrl(true, url)).json();
const response = await ky.get<T>(this.buildRequestUrl(!isServer(), url));
if (response.headers.has("X-RateLimit-Remaining")) {
this.log(`Rate limit remaining: ${response.headers.get("X-RateLimit-Remaining")}`);
}
return response.json();
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
return undefined;
}
}

View File

@ -0,0 +1 @@
export type Timeframe = "daily" | "weekly" | "monthly";

View File

@ -0,0 +1,331 @@
import ScoreSaberLeaderboard from "./model/leaderboard/impl/scoresaber-leaderboard";
import ScoreSaberLeaderboardToken from "./types/token/scoresaber/score-saber-leaderboard-token";
import LeaderboardDifficulty from "./model/leaderboard/leaderboard-difficulty";
import { getDifficultyFromScoreSaberDifficulty } from "./utils/scoresaber-utils";
import { MapCharacteristic } from "./types/map-characteristic";
import { LeaderboardStatus } from "./model/leaderboard/leaderboard-status";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate, parseDate } from "./utils/time-utils";
import ScoreSaberPlayerToken from "./types/token/scoresaber/score-saber-player-token";
import ScoreSaberPlayer, { ScoreSaberBadge, ScoreSaberBio } from "./player/impl/scoresaber-player";
import { PlayerHistory } from "./player/player-history";
import ky from "ky";
import { Config } from "./config";
import { getValueFromHistory } from "./utils/player-utils";
import { getPageFromRank } from "./utils/utils";
import ScoreSaberScoreToken from "./types/token/scoresaber/score-saber-score-token";
import { ScoreSaberScore } from "./model/score/impl/scoresaber-score";
import { Modifier } from "./score/modifier";
/**
* Parses a {@link ScoreSaberLeaderboardToken} into a {@link ScoreSaberLeaderboard}.
*
* @param token the token to parse
*/
export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardToken): ScoreSaberLeaderboard {
const difficulty: LeaderboardDifficulty = {
leaderboardId: token.difficulty.leaderboardId,
difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty),
characteristic: token.difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
difficultyRaw: token.difficulty.difficultyRaw,
};
let status: LeaderboardStatus = "Unranked";
if (token.qualified) {
status = "Qualified";
} else if (token.ranked) {
status = "Ranked";
}
return {
id: token.id,
songHash: token.songHash.toUpperCase(),
songName: token.songName,
songSubName: token.songSubName,
songAuthorName: token.songAuthorName,
levelAuthorName: token.levelAuthorName,
difficulty: difficulty,
difficulties:
token.difficulties != undefined && token.difficulties.length > 0
? token.difficulties.map(difficulty => {
return {
leaderboardId: difficulty.leaderboardId,
difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty),
characteristic: difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
difficultyRaw: difficulty.difficultyRaw,
};
})
: [difficulty],
maxScore: token.maxScore,
ranked: token.ranked,
songArt: token.coverImage,
timestamp: parseDate(token.createdDate),
stars: token.stars,
plays: token.plays,
dailyPlays: token.dailyPlays,
qualified: token.qualified,
status: status,
};
}
/**
* Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}.
*
* @param token the token to convert
* @param playerId the id of the player who set the score
* @param leaderboard the leaderboard the score was set on
*/
export function getScoreSaberScoreFromToken(
token: ScoreSaberScoreToken,
leaderboard: ScoreSaberLeaderboard,
playerId?: string
): ScoreSaberScore {
const modifiers: Modifier[] =
token.modifiers == undefined || token.modifiers === ""
? []
: token.modifiers.split(",").map(mod => {
mod = mod.toUpperCase();
const modifier = Modifier[mod as keyof typeof Modifier];
if (modifier === undefined) {
throw new Error(`Unknown modifier: ${mod}`);
}
return modifier;
});
return {
playerId: playerId || token.leaderboardPlayerInfo.id,
leaderboardId: leaderboard.id,
difficulty: leaderboard.difficulty.difficulty,
characteristic: leaderboard.difficulty.characteristic,
score: token.baseScore,
accuracy: leaderboard.maxScore ? (token.baseScore / leaderboard.maxScore) * 100 : Infinity,
rank: token.rank,
modifiers: modifiers,
misses: token.missedNotes + token.badCuts,
missedNotes: token.missedNotes,
badCuts: token.badCuts,
fullCombo: token.fullCombo,
timestamp: new Date(token.timeSet),
scoreId: token.id,
pp: token.pp,
weight: token.weight,
maxCombo: token.maxCombo,
playerInfo: token.leaderboardPlayerInfo,
};
}
/**
* Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}.
*
* @param token the player token
* @param playerIdCookie the id of the claimed player
*/
export async function getScoreSaberPlayerFromToken(
token: ScoreSaberPlayerToken,
playerIdCookie?: string
): Promise<ScoreSaberPlayer> {
const bio: ScoreSaberBio = {
lines: token.bio?.split("\n") || [],
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [], // strips html tags
};
const badges: ScoreSaberBadge[] =
token.badges?.map(badge => {
return {
url: badge.image,
description: badge.description,
};
}) || [];
let isBeingTracked = false;
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
let statisticHistory: { [key: string]: PlayerHistory } = {};
try {
const { statistics: history } = await ky
.get<{
statistics: { [key: string]: PlayerHistory };
}>(
`${Config.apiUrl}/player/history/${token.id}/50/${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}`
)
.json();
if (history) {
// Use the latest data for today
history[todayDate] = {
...history[todayDate],
rank: token.rank,
countryRank: token.countryRank,
pp: token.pp,
replaysWatched: token.scoreStats.replaysWatched,
accuracy: {
...history[todayDate]?.accuracy,
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
},
scores: {
...history[todayDate]?.scores,
totalScores: token.scoreStats.totalPlayCount,
totalRankedScores: token.scoreStats.rankedPlayCount,
},
score: {
...history[todayDate]?.score,
totalScore: token.scoreStats.totalScore,
totalRankedScore: token.scoreStats.totalRankedScore,
},
};
isBeingTracked = true;
}
statisticHistory = history;
} catch (e) {}
const playerRankHistory = token.histories.split(",").map(value => {
return parseInt(value);
});
playerRankHistory.push(token.rank);
let missingDays = 0;
let daysAgo = 0; // Start from current day
for (let i = playerRankHistory.length - 1; i >= 0; i--) {
const rank = playerRankHistory[i];
if (rank == 999_999) {
continue;
}
const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo));
daysAgo += 1;
const dateKey = formatDateMinimal(date);
if (!statisticHistory[dateKey] || statisticHistory[dateKey].rank == undefined) {
missingDays += 1;
statisticHistory[dateKey] = {
...statisticHistory[dateKey],
rank: rank,
};
}
}
if (missingDays > 0 && missingDays != playerRankHistory.length) {
console.log(
`Player has ${missingDays} missing day${missingDays > 1 ? "s" : ""}, filling in with fallback history...`
);
}
// Sort the fallback history
statisticHistory = Object.entries(statisticHistory)
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
/**
* Gets the change in the given stat
*
* @param statType the stat to check
* @param isNegativeChange whether to multiply the change by 1 or -1
* @param daysAgo the amount of days ago to get the stat for
* @return the change
*/
const getStatisticChange = (statType: string, isNegativeChange: boolean, daysAgo: number = 1): number | undefined => {
const todayStats = statisticHistory[todayDate];
let otherDate: Date | undefined;
if (daysAgo === 1) {
otherDate = getMidnightAlignedDate(getDaysAgoDate(1)); // Yesterday
} else {
const targetDate = getDaysAgoDate(daysAgo);
// Filter available dates to find the closest one to the target
const availableDates = Object.keys(statisticHistory)
.map(dateKey => new Date(dateKey))
.filter(date => {
// Convert date back to the correct format for statisticHistory lookup
const formattedDate = formatDateMinimal(date);
const statsForDate = statisticHistory[formattedDate];
const hasStat = statsForDate && statType in statsForDate;
// Only consider past dates with the required statType
const isPast = date.getTime() < new Date().getTime();
return hasStat && isPast;
});
// If no valid dates are found, return undefined
if (availableDates.length === 0) {
return undefined;
}
// Find the closest date from the filtered available dates
otherDate = availableDates.reduce((closestDate, currentDate) => {
const currentDiff = Math.abs(currentDate.getTime() - targetDate.getTime());
const closestDiff = Math.abs(closestDate.getTime() - targetDate.getTime());
return currentDiff < closestDiff ? currentDate : closestDate;
}, availableDates[0]); // Start with the first available date
}
// Ensure todayStats exists and contains the statType
if (!todayStats || !getValueFromHistory(todayStats, statType)) {
return undefined;
}
const otherStats = statisticHistory[formatDateMinimal(otherDate)]; // This is now validated
// Ensure otherStats exists and contains the statType
if (!otherStats || !getValueFromHistory(otherStats, statType)) {
return undefined;
}
const statToday = getValueFromHistory(todayStats, statType);
const statOther = getValueFromHistory(otherStats, statType);
if (statToday === undefined || statOther === undefined) {
return undefined;
}
return (statToday - statOther) * (!isNegativeChange ? 1 : -1);
};
const getStatisticChanges = (daysAgo: number): PlayerHistory => {
return {
rank: getStatisticChange("rank", true, daysAgo),
countryRank: getStatisticChange("countryRank", true, daysAgo),
pp: getStatisticChange("pp", false, daysAgo),
replaysWatched: getStatisticChange("replaysWatched", false, daysAgo),
accuracy: {
averageRankedAccuracy: getStatisticChange("accuracy.averageRankedAccuracy", false, daysAgo),
},
score: {
totalScore: getStatisticChange("score.totalScore", false, daysAgo),
totalRankedScore: getStatisticChange("score.totalRankedScore", false, daysAgo),
},
scores: {
totalScores: getStatisticChange("scores.totalScores", false, daysAgo),
totalRankedScores: getStatisticChange("scores.totalRankedScores", false, daysAgo),
rankedScores: getStatisticChange("scores.rankedScores", false, daysAgo),
unrankedScores: getStatisticChange("scores.unrankedScores", false, daysAgo),
},
};
};
return {
id: token.id,
name: token.name,
avatar: token.profilePicture,
country: token.country,
rank: token.rank,
countryRank: token.countryRank,
joinedDate: new Date(token.firstSeen),
bio: bio,
pp: token.pp,
statisticChange: {
daily: getStatisticChanges(1),
weekly: getStatisticChanges(7),
monthly: getStatisticChanges(30),
},
role: token.role == null ? undefined : token.role,
badges: badges,
statisticHistory: statisticHistory,
statistics: token.scoreStats,
rankPages: {
global: getPageFromRank(token.rank, 50),
country: getPageFromRank(token.countryRank, 50),
},
permissions: token.permissions,
banned: token.banned,
inactive: token.inactive,
isBeingTracked: isBeingTracked,
};
}

View File

@ -0,0 +1 @@
export type AroundPlayer = "global" | "country";

View File

@ -0,0 +1,26 @@
export type AppStatistics = {
/**
* The total amount of players being tracked.
*/
trackedPlayers: number;
/**
* The total amount of ScoreSaber scores tracked.
*/
trackedScores: number;
/**
* The total amount of additional data for scores being tracked.
*/
additionalScoresData: number;
/**
* The amount of cached BeatSaver maps.
*/
cachedBeatSaverMaps: number;
/**
* The amount of cached ScoreSaber leaderboards.
*/
cachedScoreSaberLeaderboards: number;
};

View File

@ -0,0 +1 @@
export type MapCharacteristic = "Standard" | "Lawless";

View File

@ -0,0 +1,28 @@
export class Metadata {
/**
* The amount of pages in the pagination
*/
public readonly totalPages: number;
/**
* The total amount of items
*/
public readonly totalItems: number;
/**
* The current page
*/
public readonly page: number;
/**
* The amount of items per page
*/
public readonly itemsPerPage: number;
constructor(totalPages: number, totalItems: number, page: number, itemsPerPage: number) {
this.totalPages = totalPages;
this.totalItems = totalItems;
this.page = page;
this.itemsPerPage = itemsPerPage;
}
}

View File

@ -1,264 +0,0 @@
import Player, { StatisticChange } from "../player";
import ky from "ky";
import { PlayerHistory } from "../player-history";
import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils";
/**
* A ScoreSaber player.
*/
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: ScoreSaberRole | 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;
/**
* 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;
}
/**
* Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}.
*
* @param token the player token
* @param apiUrl the api url for SSR
* @param playerIdCookie the player id cookie (doesn't need to be set)
*/
export async function getScoreSaberPlayerFromToken(
token: ScoreSaberPlayerToken,
apiUrl: string,
playerIdCookie?: string
): Promise<ScoreSaberPlayer> {
const bio: ScoreSaberBio = {
lines: token.bio?.split("\n") || [],
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [],
};
const role = token.role == null ? undefined : (token.role as ScoreSaberRole);
const badges: ScoreSaberBadge[] =
token.badges?.map(badge => {
return {
url: badge.image,
description: badge.description,
};
}) || [];
let isBeingTracked = false;
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
let statisticHistory: { [key: string]: PlayerHistory } = {};
try {
const { statistics: history } = await ky
.get<{
statistics: { [key: string]: PlayerHistory };
}>(
`${apiUrl}/player/history/${token.id}${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}`
)
.json();
if (history === undefined || Object.entries(history).length === 0) {
console.log("Player has no history, using fallback");
throw new Error();
}
if (history) {
// Use the latest data for today
history[todayDate] = {
rank: token.rank,
countryRank: token.countryRank,
pp: token.pp,
accuracy: {
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
},
};
isBeingTracked = true;
}
statisticHistory = history;
} catch (error) {
// Fallback to ScoreSaber History if the player has no history
const playerRankHistory = token.histories.split(",").map(value => {
return parseInt(value);
});
playerRankHistory.push(token.rank);
let daysAgo = 0; // Start from current day
for (let i = playerRankHistory.length - 1; i >= 0; i--) {
const rank = playerRankHistory[i];
const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo));
daysAgo += 1; // Increment daysAgo for each earlier rank
statisticHistory[formatDateMinimal(date)] = {
rank: rank,
};
}
}
// Sort the fallback history
statisticHistory = Object.entries(statisticHistory)
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
const yesterdayDate = formatDateMinimal(getMidnightAlignedDate(getDaysAgoDate(1)));
const todayStats = statisticHistory[todayDate];
const yesterdayStats = statisticHistory[yesterdayDate];
const hasChange = !!(todayStats && yesterdayStats);
/**
* Gets the change in the given stat
*
* @param statType the stat to check
* @return the change
*/
const getChange = (statType: "rank" | "countryRank" | "pp"): number => {
if (!hasChange) {
return 0;
}
const statToday = todayStats[`${statType}`];
const statYesterday = yesterdayStats[`${statType}`];
return !!(statToday && statYesterday) ? statToday - statYesterday : 0;
};
// Calculate the changes
const rankChange = getChange("rank");
const countryRankChange = getChange("countryRank");
const ppChange = getChange("pp");
return {
id: token.id,
name: token.name,
avatar: token.profilePicture,
country: token.country,
rank: token.rank,
countryRank: token.countryRank,
joinedDate: new Date(token.firstSeen),
bio: bio,
pp: token.pp,
statisticChange: {
rank: rankChange * -1, // Reverse the rank change
countryRank: countryRankChange * -1, // Reverse the country rank change
pp: ppChange,
},
role: role,
badges: badges,
statisticHistory: statisticHistory,
statistics: token.scoreStats,
permissions: token.permissions,
banned: token.banned,
inactive: token.inactive,
isBeingTracked: isBeingTracked,
};
}
/**
* 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[];
};
/**
* The ScoreSaber account roles.
*/
export type ScoreSaberRole = "Admin";
/**
* 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;
};

View File

@ -1,26 +0,0 @@
export interface PlayerHistory {
/**
* The player's rank.
*/
rank?: number;
/**
* The player's country rank.
*/
countryRank?: number;
/**
* The pp of the player.
*/
pp?: number;
/**
* The player's accuracy.
*/
accuracy?: {
/**
* The player's average ranked accuracy.
*/
averageRankedAccuracy?: number;
};
}

View File

@ -1,47 +0,0 @@
import Score from "../score";
import { Modifier } from "../modifier";
import ScoreSaberScoreToken from "../../token/scoresaber/score-saber-score-token";
export default class ScoreSaberScore extends Score {
constructor(
score: number,
weight: number | undefined,
rank: number,
worth: number,
modifiers: Modifier[],
misses: number,
badCuts: number,
fullCombo: boolean,
timestamp: Date
) {
super(score, weight, rank, worth, modifiers, misses, badCuts, fullCombo, timestamp);
}
/**
* Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}.
*
* @param token the token to convert
*/
public static fromToken(token: ScoreSaberScoreToken): ScoreSaberScore {
const modifiers: Modifier[] = token.modifiers.split(",").map(mod => {
mod = mod.toUpperCase();
const modifier = Modifier[mod as keyof typeof Modifier];
if (modifier === undefined) {
throw new Error(`Unknown modifier: ${mod}`);
}
return modifier;
});
return new ScoreSaberScore(
token.baseScore,
token.weight,
token.rank,
token.pp,
modifiers,
token.missedNotes,
token.badCuts,
token.fullCombo,
new Date(token.timeSet)
);
}
}

View File

@ -1,116 +0,0 @@
import { Modifier } from "./modifier";
export default class Score {
/**
* The base score for the score.
* @private
*/
private readonly _score: number;
/**
* The weight of the score, or undefined if not ranked.s
* @private
*/
private readonly _weight: number | undefined;
/**
* The rank for the score.
* @private
*/
private readonly _rank: number;
/**
* The worth of the score (this could be pp, ap, cr, etc.),
* or undefined if not ranked.
* @private
*/
private readonly _worth: number;
/**
* The modifiers used on the score.
* @private
*/
private readonly _modifiers: Modifier[];
/**
* The amount missed notes.
* @private
*/
private readonly _misses: number;
/**
* The amount of bad cuts.
* @private
*/
private readonly _badCuts: number;
/**
* Whether every note was hit.
* @private
*/
private readonly _fullCombo: boolean;
/**
* The time the score was set.
* @private
*/
private readonly _timestamp: Date;
constructor(
score: number,
weight: number | undefined,
rank: number,
worth: number,
modifiers: Modifier[],
misses: number,
badCuts: number,
fullCombo: boolean,
timestamp: Date
) {
this._score = score;
this._weight = weight;
this._rank = rank;
this._worth = worth;
this._modifiers = modifiers;
this._misses = misses;
this._badCuts = badCuts;
this._fullCombo = fullCombo;
this._timestamp = timestamp;
}
get score(): number {
return this._score;
}
get weight(): number | undefined {
return this._weight;
}
get rank(): number {
return this._rank;
}
get worth(): number {
return this._worth;
}
get modifiers(): Modifier[] {
return this._modifiers;
}
get misses(): number {
return this._misses;
}
get badCuts(): number {
return this._badCuts;
}
get fullCombo(): boolean {
return this._fullCombo;
}
get timestamp(): Date {
return this._timestamp;
}
}

View File

@ -0,0 +1,30 @@
import { BeatLeaderModifierToken } from "./modifier/modifiers";
import { BeatLeaderModifierRatingToken } from "./modifier/modifier-rating";
export type BeatLeaderDifficultyToken = {
id: number;
value: number;
mode: number;
difficultyName: string;
modeName: string;
status: number;
modifierValues: BeatLeaderModifierToken;
modifiersRating: BeatLeaderModifierRatingToken;
nominatedTime: number;
qualifiedTime: number;
rankedTime: number;
stars: number;
predictedAcc: number;
passRating: number;
accRating: number;
techRating: number;
type: number;
njs: number;
nps: number;
notes: number;
bombs: number;
walls: number;
maxScore: number;
duration: number;
requirements: number;
};

View File

@ -0,0 +1,16 @@
import { BeatLeaderSongToken } from "./score/song";
import { BeatLeaderDifficultyToken } from "./difficulty";
export type BeatLeaderLeaderboardToken = {
id: string;
song: BeatLeaderSongToken;
difficulty: BeatLeaderDifficultyToken;
scores: null; // ??
changes: null; // ??
qualification: null; // ??
reweight: null; // ??
leaderboardGroup: null; // ??
plays: number;
clan: null; // ??
clanRankingContested: boolean;
};

View File

@ -0,0 +1,18 @@
export type BeatLeaderModifierRatingToken = {
id: number;
fsPredictedAcc: number;
fsPassRating: number;
fsAccRating: number;
fsTechRating: number;
fsStars: number;
ssPredictedAcc: number;
ssPassRating: number;
ssAccRating: number;
ssTechRating: number;
ssStars: number;
sfPredictedAcc: number;
sfPassRating: number;
sfAccRating: number;
sfTechRating: number;
sfStars: number;
};

View File

@ -0,0 +1,16 @@
export type BeatLeaderModifierToken = {
modifierId: number;
da: number;
fs: number;
sf: number;
ss: number;
gn: number;
na: number;
nb: number;
nf: number;
no: number;
pm: number;
sc: number;
sa: number;
op: number;
};

View File

@ -0,0 +1,10 @@
export type BeatLeaderPlayerToken = {
id: string;
country: string;
avatar: string;
pp: number;
rank: number;
countryRank: number;
name: string;
// todo: finish this
};

View File

@ -0,0 +1,66 @@
export type ScoreStatsAccuracyTrackerToken = {
/**
* The accuracy of the right hand.
*/
accRight: number;
/**
* The accuracy of the left hand.
*/
accLeft: number;
/**
* The left hand pre-swing.
*/
leftPreswing: number;
/**
* The right hand pre-swing.
*/
rightPreswing: number;
/**
* The average pre-swing.
*/
averagePreswing: number;
/**
* The left hand post-swing.
*/
leftPostswing: number;
/**
* The right hand post-swing.
*/
rightPostswing: number;
/**
* The left hand time dependence.
*/
leftTimeDependence: number;
/**
* The right hand time dependence.
*/
rightTimeDependence: number;
/**
* The left hand average cut.
*/
leftAverageCut: number[];
/**
* The right hand average cut.
*/
rightAverageCut: number[];
/**
* The grid accuracy.
*/
gridAcc: number[];
/**
* The full combo accuracy.
*/
fcAcc: number;
};

View File

@ -0,0 +1,16 @@
export type ScoreStatsHeadPositionToken = {
/**
* The X position of the head
*/
x: number;
/**
* The Y position of the head
*/
y: number;
/**
* The Z position of the head
*/
z: number;
};

View File

@ -0,0 +1,51 @@
export type ScoreStatsHitTrackerToken = {
/**
* The maximum combo achieved.
*/
maxCombo: number;
/**
* The highest amount of 115 notes hit in a row.
*/
maxStreak: number;
/**
* The left hand timing.
*/
leftTiming: number;
/**
* The right hand timing.
*/
rightTiming: number;
/**
* The left hand misses.
*/
leftMiss: number;
/**
* The right hand misses.
*/
rightMiss: number;
/**
* The left hand bad cuts.
*/
leftBadCuts: number;
/**
* The right hand bad cuts.
*/
rightBadCuts: number;
/**
* The left hand bombs.
*/
leftBombs: number;
/**
* The right hand bombs.
*/
rightBombs: number;
};

View File

@ -0,0 +1,6 @@
export type ScoreStatsGraphTrackerToken = {
/**
* The accuracy graph data.
*/
graph: number[];
};

View File

@ -0,0 +1,26 @@
import { ScoreStatsHitTrackerToken } from "./hit-tracker";
import { ScoreStatsAccuracyTrackerToken } from "./accuracy-tracker";
import { ScoreStatsWinTrackerToken } from "./win-tracker";
import { ScoreStatsGraphTrackerToken } from "./score-graph-tracker";
export type ScoreStatsToken = {
/**
* The hit tracker stats.
*/
hitTracker: ScoreStatsHitTrackerToken;
/**
* The accuracy tracker stats.
*/
accuracyTracker: ScoreStatsAccuracyTrackerToken;
/**
* The win tracker stats.
*/
winTracker: ScoreStatsWinTrackerToken;
/**
* The score graph tracker stats.
*/
scoreGraphTracker: ScoreStatsGraphTrackerToken;
};

View File

@ -0,0 +1,48 @@
import { ScoreStatsHeadPositionToken } from "./head-position";
export type ScoreStatsWinTrackerToken = {
/**
* Whether the score was won. (not failed)
*/
won: boolean;
/**
* The time the score ended.
*/
endTime: number;
/**
* The total amount of pauses.
*/
nbOfPause: number;
/**
* The total amount of pause time.
*/
totalPauseDuration: number;
/**
* The jump distance the score was played on.
*/
jumpDistance: number;
/**
* The average height of the player.
*/
averageHeight: number;
/**
* The average head position of the player.
*/
averageHeadPosition: ScoreStatsHeadPositionToken;
/**
* The total score.
*/
totalScore: number;
/**
* The maximum score for this song.
*/
maxScore: number;
};

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