rhythia-api 186.0.0 → 188.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.prettierrc.json +6 -6
  2. package/api/acceptInvite.ts +79 -0
  3. package/api/addCollectionMap.ts +82 -82
  4. package/api/approveMap.ts +78 -78
  5. package/api/chartPublicStats.ts +32 -32
  6. package/api/createBeatmap.ts +208 -168
  7. package/api/createBeatmapPage.ts +64 -64
  8. package/api/createClan.ts +81 -81
  9. package/api/createCollection.ts +58 -58
  10. package/api/createInvite.ts +66 -0
  11. package/api/deleteBeatmapPage.ts +77 -77
  12. package/api/deleteCollection.ts +59 -59
  13. package/api/deleteCollectionMap.ts +71 -71
  14. package/api/editAboutMe.ts +91 -91
  15. package/api/editClan.ts +90 -90
  16. package/api/editCollection.ts +77 -77
  17. package/api/editProfile.ts +123 -123
  18. package/api/getAvatarUploadUrl.ts +85 -85
  19. package/api/getBadgedUsers.ts +56 -56
  20. package/api/getBeatmapComments.ts +57 -57
  21. package/api/getBeatmapPage.ts +106 -106
  22. package/api/getBeatmapPageById.ts +99 -99
  23. package/api/getBeatmapStarRating.ts +53 -53
  24. package/api/getBeatmaps.ts +159 -159
  25. package/api/getClan.ts +77 -77
  26. package/api/getClans.ts +44 -0
  27. package/api/getCollection.ts +130 -130
  28. package/api/getCollections.ts +132 -132
  29. package/api/getLeaderboard.ts +136 -136
  30. package/api/getMapUploadUrl.ts +93 -93
  31. package/api/getPassToken.ts +55 -55
  32. package/api/getProfile.ts +146 -146
  33. package/api/getPublicStats.ts +180 -180
  34. package/api/getRawStarRating.ts +57 -57
  35. package/api/getScore.ts +85 -85
  36. package/api/getTimestamp.ts +23 -23
  37. package/api/getUserScores.ts +175 -175
  38. package/api/nominateMap.ts +82 -82
  39. package/api/postBeatmapComment.ts +62 -59
  40. package/api/rankMapsArchive.ts +64 -64
  41. package/api/searchUsers.ts +56 -56
  42. package/api/setPasskey.ts +59 -59
  43. package/api/submitScore.ts +433 -433
  44. package/api/updateBeatmapPage.ts +229 -229
  45. package/handleApi.ts +21 -20
  46. package/index.html +2 -2
  47. package/index.ts +914 -863
  48. package/package.json +5 -2
  49. package/types/database.ts +43 -0
  50. package/utils/getUserBySession.ts +48 -48
  51. package/utils/requestUtils.ts +87 -87
  52. package/utils/security.ts +20 -20
  53. package/utils/star-calc/index.ts +72 -72
  54. package/utils/star-calc/osuUtils.ts +53 -53
  55. package/utils/star-calc/sspmParser.ts +398 -398
  56. package/utils/star-calc/sspmv1Parser.ts +165 -165
  57. package/utils/supabase.ts +13 -13
  58. package/utils/test +4 -4
  59. package/utils/validateToken.ts +7 -7
  60. package/vercel.json +12 -12
package/api/getProfile.ts CHANGED
@@ -1,146 +1,146 @@
1
- import { geolocation } from "@vercel/edge";
2
- import { NextResponse } from "next/server";
3
- import z from "zod";
4
- import { Database } from "../types/database";
5
- import { protectedApi } from "../utils/requestUtils";
6
- import { supabase } from "../utils/supabase";
7
- import { getUserBySession } from "../utils/getUserBySession";
8
- import { User } from "@supabase/supabase-js";
9
-
10
- export const Schema = {
11
- input: z.strictObject({
12
- session: z.string(),
13
- id: z.number().nullable().optional(),
14
- }),
15
- output: z.object({
16
- error: z.string().optional(),
17
- user: z
18
- .object({
19
- about_me: z.string().nullable(),
20
- avatar_url: z.string().nullable(),
21
- profile_image: z.string().nullable(),
22
- badges: z.any().nullable(),
23
- created_at: z.number().nullable(),
24
- flag: z.string().nullable(),
25
- id: z.number(),
26
- uid: z.string().nullable(),
27
- ban: z.string().nullable(),
28
- username: z.string().nullable(),
29
- verified: z.boolean().nullable(),
30
- play_count: z.number().nullable(),
31
- skill_points: z.number().nullable(),
32
- squares_hit: z.number().nullable(),
33
- total_score: z.number().nullable(),
34
- position: z.number().nullable(),
35
- is_online: z.boolean(),
36
- clans: z
37
- .object({
38
- id: z.number(),
39
- acronym: z.string(),
40
- })
41
- .optional()
42
- .nullable(),
43
- })
44
- .optional(),
45
- }),
46
- };
47
-
48
- export async function POST(request: Request): Promise<NextResponse> {
49
- return protectedApi({
50
- request,
51
- schema: Schema,
52
- authorization: () => {},
53
- activity: handler,
54
- });
55
- }
56
-
57
- export async function handler(
58
- data: (typeof Schema)["input"]["_type"],
59
- req: Request
60
- ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
61
- let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
62
- let isOnline = false;
63
- // Fetch by id
64
- if (data.id !== undefined && data.id !== null) {
65
- let { data: queryData, error } = await supabase
66
- .from("profiles")
67
- .select(`*,clans:clan(id,acronym)`)
68
- .eq("id", data.id);
69
-
70
- console.log(profiles, error);
71
-
72
- if (!queryData?.length) {
73
- return NextResponse.json(
74
- {
75
- error: "User not found",
76
- },
77
- { status: 404 }
78
- );
79
- }
80
-
81
- profiles = queryData;
82
- } else {
83
- // Fetch by session id
84
- const user = (await getUserBySession(data.session)) as User;
85
-
86
- if (user) {
87
- let { data: queryData, error } = await supabase
88
- .from("profiles")
89
- .select("*")
90
- .eq("uid", user.id);
91
-
92
- if (!queryData?.length) {
93
- const geo = geolocation(req);
94
- const data = await supabase.from("profiles").upsert({
95
- uid: user.id,
96
- about_me: "",
97
- avatar_url:
98
- "https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
99
- badges: [],
100
- username: `${user.user_metadata.full_name.slice(0, 20)}${Math.round(
101
- Math.random() * 900000 + 100000
102
- )}`,
103
- computedUsername: `${user.user_metadata.full_name.slice(
104
- 0,
105
- 20
106
- )}${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
107
- flag: (geo.country || "US").toUpperCase(),
108
- created_at: Date.now(),
109
- }).select(`
110
- *,clans:clan(id,acronym)`);
111
-
112
- profiles = data.data!;
113
- } else {
114
- profiles = queryData;
115
- }
116
- }
117
- }
118
-
119
- const user = profiles[0];
120
-
121
- const { data: activityData } = await supabase
122
- .from("profileActivities")
123
- .select("*")
124
- .eq("uid", user.uid || "")
125
- .single();
126
-
127
- //last 30 minutes
128
- if (activityData && activityData.last_activity) {
129
- isOnline = Date.now() - activityData.last_activity < 1800000;
130
- }
131
-
132
- // Query to count how many players have more skill points than the specific player
133
- const { count: playersWithMorePoints, error: rankError } = await supabase
134
- .from("profiles")
135
- .select(`*`, { count: "exact", head: true })
136
- .neq("ban", "excluded")
137
- .gt("skill_points", user.skill_points);
138
-
139
- return NextResponse.json({
140
- user: {
141
- ...user,
142
- position: (playersWithMorePoints || 0) + 1,
143
- is_online: isOnline,
144
- },
145
- });
146
- }
1
+ import { geolocation } from "@vercel/edge";
2
+ import { NextResponse } from "next/server";
3
+ import z from "zod";
4
+ import { Database } from "../types/database";
5
+ import { protectedApi } from "../utils/requestUtils";
6
+ import { supabase } from "../utils/supabase";
7
+ import { getUserBySession } from "../utils/getUserBySession";
8
+ import { User } from "@supabase/supabase-js";
9
+
10
+ export const Schema = {
11
+ input: z.strictObject({
12
+ session: z.string(),
13
+ id: z.number().nullable().optional(),
14
+ }),
15
+ output: z.object({
16
+ error: z.string().optional(),
17
+ user: z
18
+ .object({
19
+ about_me: z.string().nullable(),
20
+ avatar_url: z.string().nullable(),
21
+ profile_image: z.string().nullable(),
22
+ badges: z.any().nullable(),
23
+ created_at: z.number().nullable(),
24
+ flag: z.string().nullable(),
25
+ id: z.number(),
26
+ uid: z.string().nullable(),
27
+ ban: z.string().nullable(),
28
+ username: z.string().nullable(),
29
+ verified: z.boolean().nullable(),
30
+ play_count: z.number().nullable(),
31
+ skill_points: z.number().nullable(),
32
+ squares_hit: z.number().nullable(),
33
+ total_score: z.number().nullable(),
34
+ position: z.number().nullable(),
35
+ is_online: z.boolean(),
36
+ clans: z
37
+ .object({
38
+ id: z.number(),
39
+ acronym: z.string(),
40
+ })
41
+ .optional()
42
+ .nullable(),
43
+ })
44
+ .optional(),
45
+ }),
46
+ };
47
+
48
+ export async function POST(request: Request): Promise<NextResponse> {
49
+ return protectedApi({
50
+ request,
51
+ schema: Schema,
52
+ authorization: () => {},
53
+ activity: handler,
54
+ });
55
+ }
56
+
57
+ export async function handler(
58
+ data: (typeof Schema)["input"]["_type"],
59
+ req: Request
60
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
61
+ let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
62
+ let isOnline = false;
63
+ // Fetch by id
64
+ if (data.id !== undefined && data.id !== null) {
65
+ let { data: queryData, error } = await supabase
66
+ .from("profiles")
67
+ .select(`*,clans:clan(id,acronym)`)
68
+ .eq("id", data.id);
69
+
70
+ console.log(profiles, error);
71
+
72
+ if (!queryData?.length) {
73
+ return NextResponse.json(
74
+ {
75
+ error: "User not found",
76
+ },
77
+ { status: 404 }
78
+ );
79
+ }
80
+
81
+ profiles = queryData;
82
+ } else {
83
+ // Fetch by session id
84
+ const user = (await getUserBySession(data.session)) as User;
85
+
86
+ if (user) {
87
+ let { data: queryData, error } = await supabase
88
+ .from("profiles")
89
+ .select("*")
90
+ .eq("uid", user.id);
91
+
92
+ if (!queryData?.length) {
93
+ const geo = geolocation(req);
94
+ const data = await supabase.from("profiles").upsert({
95
+ uid: user.id,
96
+ about_me: "",
97
+ avatar_url:
98
+ "https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
99
+ badges: [],
100
+ username: `${user.user_metadata.full_name.slice(0, 20)}${Math.round(
101
+ Math.random() * 900000 + 100000
102
+ )}`,
103
+ computedUsername: `${user.user_metadata.full_name.slice(
104
+ 0,
105
+ 20
106
+ )}${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
107
+ flag: (geo.country || "US").toUpperCase(),
108
+ created_at: Date.now(),
109
+ }).select(`
110
+ *,clans:clan(id,acronym)`);
111
+
112
+ profiles = data.data!;
113
+ } else {
114
+ profiles = queryData;
115
+ }
116
+ }
117
+ }
118
+
119
+ const user = profiles[0];
120
+
121
+ const { data: activityData } = await supabase
122
+ .from("profileActivities")
123
+ .select("*")
124
+ .eq("uid", user.uid || "")
125
+ .single();
126
+
127
+ //last 30 minutes
128
+ if (activityData && activityData.last_activity) {
129
+ isOnline = Date.now() - activityData.last_activity < 1800000;
130
+ }
131
+
132
+ // Query to count how many players have more skill points than the specific player
133
+ const { count: playersWithMorePoints, error: rankError } = await supabase
134
+ .from("profiles")
135
+ .select(`*`, { count: "exact", head: true })
136
+ .neq("ban", "excluded")
137
+ .gt("skill_points", user.skill_points);
138
+
139
+ return NextResponse.json({
140
+ user: {
141
+ ...user,
142
+ position: (playersWithMorePoints || 0) + 1,
143
+ is_online: isOnline,
144
+ },
145
+ });
146
+ }
@@ -1,180 +1,180 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { protectedApi } from "../utils/requestUtils";
4
- import { supabase } from "../utils/supabase";
5
-
6
- export const Schema = {
7
- input: z.strictObject({}),
8
- output: z.object({
9
- profiles: z.number(),
10
- beatmaps: z.number(),
11
- scores: z.number(),
12
- onlineUsers: z.number(),
13
- countChart: z.array(
14
- z.object({
15
- type: z.string(),
16
- value: z.number(),
17
- })
18
- ),
19
- lastBeatmaps: z.array(
20
- z.object({
21
- id: z.number().nullable().optional(),
22
- nominations: z.array(z.number()).nullable().optional(),
23
- playcount: z.number().nullable().optional(),
24
- created_at: z.string().nullable().optional(),
25
- difficulty: z.number().nullable().optional(),
26
- noteCount: z.number().nullable().optional(),
27
- length: z.number().nullable().optional(),
28
- title: z.string().nullable().optional(),
29
- ranked: z.boolean().nullable().optional(),
30
- beatmapFile: z.string().nullable().optional(),
31
- image: z.string().nullable().optional(),
32
- starRating: z.number().nullable().optional(),
33
- owner: z.number().nullable().optional(),
34
- ownerUsername: z.string().nullable().optional(),
35
- ownerAvatar: z.string().nullable().optional(),
36
- status: z.string().nullable().optional(),
37
- })
38
- ),
39
- topUsers: z.array(
40
- z.object({
41
- username: z.string(),
42
- id: z.number(),
43
- avatar_url: z.string(),
44
- skill_points: z.number(),
45
- })
46
- ),
47
- lastComments: z.array(
48
- z.object({
49
- owner: z.number(),
50
- content: z.string(),
51
- username: z.string(),
52
- beatmapTitle: z.string(),
53
- beatmapPage: z.number(),
54
- })
55
- ),
56
- }),
57
- };
58
-
59
- export async function POST(request: Request) {
60
- return protectedApi({
61
- request,
62
- schema: Schema,
63
- authorization: () => {},
64
- activity: handler,
65
- });
66
- }
67
-
68
- export async function handler(data: (typeof Schema)["input"]["_type"]) {
69
- const countProfilesQuery = await supabase
70
- .from("profiles")
71
- .select("*", { count: "exact", head: true });
72
-
73
- const countBeatmapsQuery = await supabase
74
- .from("beatmaps")
75
- .select("*", { count: "exact", head: true });
76
-
77
- let { data: beatmapPage, error: errorlast } = await supabase
78
- .from("beatmapPages")
79
- .select(
80
- `
81
- *,
82
- beatmaps (
83
- created_at,
84
- playcount,
85
- length,
86
- ranked,
87
- beatmapFile,
88
- image,
89
- starRating,
90
- difficulty,
91
- noteCount,
92
- title
93
- ),
94
- profiles (
95
- username,
96
- avatar_url
97
- )
98
- `
99
- )
100
- .eq("status", "RANKED")
101
- .order("ranked_at", { ascending: false })
102
- .limit(4);
103
-
104
- let { data: topUsers } = await supabase
105
- .from("profiles")
106
- .select("*")
107
- .neq("ban", "excluded")
108
- .order("skill_points", { ascending: false })
109
- .limit(3);
110
-
111
- let { data: comments } = await supabase
112
- .from("beatmapPageComments")
113
- .select(
114
- `
115
- *,
116
- beatmapPages!inner(
117
- *
118
- ),
119
- profiles!inner(
120
- username
121
- )`
122
- )
123
- .order("created_at", { ascending: false })
124
- .limit(5);
125
-
126
- const countScoresQuery = await supabase
127
- .from("scores")
128
- .select("id", { count: "exact", head: true });
129
-
130
- // 30 minutes activity
131
- const countOnline = await supabase
132
- .from("profileActivities")
133
- .select("*", { count: "exact", head: true })
134
- .gt("last_activity", Date.now() - 1800000);
135
-
136
- const countChart = await supabase
137
- .from("chartedValues")
138
- .select("value")
139
- .eq("type", "online_players")
140
- .gt("created_at", new Date(Date.now() - 86400000).toISOString());
141
-
142
- return NextResponse.json({
143
- beatmaps: countBeatmapsQuery.count,
144
- profiles: countProfilesQuery.count,
145
- scores: countScoresQuery.count,
146
- onlineUsers: countOnline.count,
147
- countChart: countChart.data,
148
- lastBeatmaps: beatmapPage?.map((e) => ({
149
- playcount: e.beatmaps?.playcount,
150
- created_at: e.created_at,
151
- difficulty: e.beatmaps?.difficulty,
152
- noteCount: e.beatmaps?.noteCount,
153
- length: e.beatmaps?.length,
154
- title: e.beatmaps?.title,
155
- ranked: e.beatmaps?.ranked,
156
- beatmapFile: e.beatmaps?.beatmapFile,
157
- image: e.beatmaps?.image,
158
- starRating: e.beatmaps?.starRating,
159
- owner: e.owner,
160
- ownerUsername: e.profiles?.username,
161
- ownerAvatar: e.profiles?.avatar_url,
162
- id: e.id,
163
- status: e.status,
164
- nominations: e.nominations as number[],
165
- })),
166
- topUsers: topUsers?.map((e) => ({
167
- username: e.username,
168
- id: e.id,
169
- avatar_url: e.avatar_url,
170
- skill_points: e.skill_points,
171
- })),
172
- lastComments: comments?.map((e) => ({
173
- owner: e.owner,
174
- content: e.content,
175
- username: e.profiles.username,
176
- beatmapTitle: e.beatmapPages.title,
177
- beatmapPage: e.beatmapPages.id,
178
- })),
179
- });
180
- }
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+
6
+ export const Schema = {
7
+ input: z.strictObject({}),
8
+ output: z.object({
9
+ profiles: z.number(),
10
+ beatmaps: z.number(),
11
+ scores: z.number(),
12
+ onlineUsers: z.number(),
13
+ countChart: z.array(
14
+ z.object({
15
+ type: z.string(),
16
+ value: z.number(),
17
+ })
18
+ ),
19
+ lastBeatmaps: z.array(
20
+ z.object({
21
+ id: z.number().nullable().optional(),
22
+ nominations: z.array(z.number()).nullable().optional(),
23
+ playcount: z.number().nullable().optional(),
24
+ created_at: z.string().nullable().optional(),
25
+ difficulty: z.number().nullable().optional(),
26
+ noteCount: z.number().nullable().optional(),
27
+ length: z.number().nullable().optional(),
28
+ title: z.string().nullable().optional(),
29
+ ranked: z.boolean().nullable().optional(),
30
+ beatmapFile: z.string().nullable().optional(),
31
+ image: z.string().nullable().optional(),
32
+ starRating: z.number().nullable().optional(),
33
+ owner: z.number().nullable().optional(),
34
+ ownerUsername: z.string().nullable().optional(),
35
+ ownerAvatar: z.string().nullable().optional(),
36
+ status: z.string().nullable().optional(),
37
+ })
38
+ ),
39
+ topUsers: z.array(
40
+ z.object({
41
+ username: z.string(),
42
+ id: z.number(),
43
+ avatar_url: z.string(),
44
+ skill_points: z.number(),
45
+ })
46
+ ),
47
+ lastComments: z.array(
48
+ z.object({
49
+ owner: z.number(),
50
+ content: z.string(),
51
+ username: z.string(),
52
+ beatmapTitle: z.string(),
53
+ beatmapPage: z.number(),
54
+ })
55
+ ),
56
+ }),
57
+ };
58
+
59
+ export async function POST(request: Request) {
60
+ return protectedApi({
61
+ request,
62
+ schema: Schema,
63
+ authorization: () => {},
64
+ activity: handler,
65
+ });
66
+ }
67
+
68
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
69
+ const countProfilesQuery = await supabase
70
+ .from("profiles")
71
+ .select("*", { count: "exact", head: true });
72
+
73
+ const countBeatmapsQuery = await supabase
74
+ .from("beatmaps")
75
+ .select("*", { count: "exact", head: true });
76
+
77
+ let { data: beatmapPage, error: errorlast } = await supabase
78
+ .from("beatmapPages")
79
+ .select(
80
+ `
81
+ *,
82
+ beatmaps (
83
+ created_at,
84
+ playcount,
85
+ length,
86
+ ranked,
87
+ beatmapFile,
88
+ image,
89
+ starRating,
90
+ difficulty,
91
+ noteCount,
92
+ title
93
+ ),
94
+ profiles (
95
+ username,
96
+ avatar_url
97
+ )
98
+ `
99
+ )
100
+ .eq("status", "RANKED")
101
+ .order("ranked_at", { ascending: false })
102
+ .limit(4);
103
+
104
+ let { data: topUsers } = await supabase
105
+ .from("profiles")
106
+ .select("*")
107
+ .neq("ban", "excluded")
108
+ .order("skill_points", { ascending: false })
109
+ .limit(3);
110
+
111
+ let { data: comments } = await supabase
112
+ .from("beatmapPageComments")
113
+ .select(
114
+ `
115
+ *,
116
+ beatmapPages!inner(
117
+ *
118
+ ),
119
+ profiles!inner(
120
+ username
121
+ )`
122
+ )
123
+ .order("created_at", { ascending: false })
124
+ .limit(5);
125
+
126
+ const countScoresQuery = await supabase
127
+ .from("scores")
128
+ .select("id", { count: "exact", head: true });
129
+
130
+ // 30 minutes activity
131
+ const countOnline = await supabase
132
+ .from("profileActivities")
133
+ .select("*", { count: "exact", head: true })
134
+ .gt("last_activity", Date.now() - 1800000);
135
+
136
+ const countChart = await supabase
137
+ .from("chartedValues")
138
+ .select("value")
139
+ .eq("type", "online_players")
140
+ .gt("created_at", new Date(Date.now() - 86400000).toISOString());
141
+
142
+ return NextResponse.json({
143
+ beatmaps: countBeatmapsQuery.count,
144
+ profiles: countProfilesQuery.count,
145
+ scores: countScoresQuery.count,
146
+ onlineUsers: countOnline.count,
147
+ countChart: countChart.data,
148
+ lastBeatmaps: beatmapPage?.map((e) => ({
149
+ playcount: e.beatmaps?.playcount,
150
+ created_at: e.created_at,
151
+ difficulty: e.beatmaps?.difficulty,
152
+ noteCount: e.beatmaps?.noteCount,
153
+ length: e.beatmaps?.length,
154
+ title: e.beatmaps?.title,
155
+ ranked: e.beatmaps?.ranked,
156
+ beatmapFile: e.beatmaps?.beatmapFile,
157
+ image: e.beatmaps?.image,
158
+ starRating: e.beatmaps?.starRating,
159
+ owner: e.owner,
160
+ ownerUsername: e.profiles?.username,
161
+ ownerAvatar: e.profiles?.avatar_url,
162
+ id: e.id,
163
+ status: e.status,
164
+ nominations: e.nominations as number[],
165
+ })),
166
+ topUsers: topUsers?.map((e) => ({
167
+ username: e.username,
168
+ id: e.id,
169
+ avatar_url: e.avatar_url,
170
+ skill_points: e.skill_points,
171
+ })),
172
+ lastComments: comments?.map((e) => ({
173
+ owner: e.owner,
174
+ content: e.content,
175
+ username: e.profiles.username,
176
+ beatmapTitle: e.beatmapPages.title,
177
+ beatmapPage: e.beatmapPages.id,
178
+ })),
179
+ });
180
+ }