rhythia-api 217.0.0 → 225.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.
package/.env ADDED
@@ -0,0 +1,12 @@
1
+ REDIS_PORT=18304
2
+ REDIS_USERNAME=default
3
+ REDIS_PASSWORD=yg7hYF9LPhGJRmORVIjMB8FWP2axPGto
4
+ REDIS_HOST=redis-18304.c55.eu-central-1-1.ec2.cloud.redislabs.com
5
+ CF_NAMESPACE_ID=571cf88650514eeba649517a75ede74a
6
+ CF_API_TOKEN=Ldsh-UKJZw0feHmZOvhSCxaurwJBVW12LgnV_v38
7
+ CF_ACCOUNT_ID=571cf88650514eeba649517a75ede74a
8
+ TOKEN_SECRET=fa686bfdffd3758f6377abbc23bf3d9bdc1a0dda4a6e7f8dbdd579fa1ff6d7e2
9
+ ACCESS_BUCKET=003c245e893e8060000000003
10
+ SECRET_BUCKET=K003LpAu8X+2lJ09EB8NtPL/OZXV8ts
11
+ ADMIN_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBma2FqbmdibGxjYmR6b3lscnZwIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcyOTAxNTMwNSwiZXhwIjoyMDQ0NTkxMzA1fQ.dKg8Wq3zZEBAH63V7q1D8a8N-d6qkB5Inm524Vmso3k
12
+ PROD_DO_NOT_WRITE_PG=postgresql://postgres:huaUpTz3d3p7w6VU@db.pfkajngbllcbdzoylrvp.supabase.co:5432/postgres
package/.yarnrc ADDED
@@ -0,0 +1 @@
1
+ registry: https://registry.npmjs.org/
package/README.md CHANGED
@@ -7,3 +7,5 @@ Install dependencies with Bun, then reach for the scripts in `package.json`; `bu
7
7
  Runtime secrets live in environment variables. Upload endpoints expect `ACCESS_BUCKET` and `SECRET_BUCKET` for S3, the purchase flow checks `BUY_SECRET`, auth helpers derive tokens from `TOKEN_SECRET`, and the Supabase admin calls need `ADMIN_KEY`. The deploy script also looks for `GIT_USER`, `GIT_KEY`, `SOURCE_BRANCH`, and `TARGET_BRANCH` when the CI job mirrors changes upstream.
8
8
 
9
9
  If you need to point at a different stack, call `setEnvironment` with `development`, `testing`, or `production` before making requests so the helper talks to the right Rhythia host.
10
+
11
+ //
@@ -102,7 +102,7 @@ export async function handler({
102
102
  .single();
103
103
 
104
104
  if (!userData) {
105
- return NextResponse.json({ error: "Bad user" });
105
+ return NextResponse.json({ error: "Bad user!" });
106
106
  }
107
107
 
108
108
  if (
@@ -1,10 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
- import { protectedApi, validUser } from "../utils/requestUtils";
4
- import { supabase } from "../utils/supabase";
5
- import { getUserBySession } from "../utils/getUserBySession";
6
- import { User } from "@supabase/supabase-js";
7
- import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+ import { getUserBySession } from "../utils/getUserBySession";
6
+ import { User } from "@supabase/supabase-js";
7
+ import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
8
8
 
9
9
  export const Schema = {
10
10
  input: z.strictObject({
@@ -67,18 +67,18 @@ export async function handler({
67
67
  if (pageData.status !== "UNRANKED")
68
68
  return NextResponse.json({ error: "Only unranked maps can be updated" });
69
69
 
70
- await supabase.from("beatmapPageComments").delete().eq("beatmapPage", id);
71
- await supabase.from("collectionRelations").delete().eq("beatmapPage", id);
72
- await supabase.from("beatmapPages").delete().eq("id", id);
73
- await supabase
74
- .from("beatmaps")
75
- .delete()
76
- .eq("beatmapHash", beatmapData.beatmapHash);
77
-
78
- await invalidateCache(`beatmap-comments:${id}`);
79
- if (pageData.latestBeatmapHash) {
80
- await invalidateCachePrefix(`beatmap-scores:${pageData.latestBeatmapHash}`);
81
- }
82
-
83
- return NextResponse.json({});
84
- }
70
+ await supabase.from("beatmapPageComments").delete().eq("beatmapPage", id);
71
+ await supabase.from("collectionRelations").delete().eq("beatmapPage", id);
72
+ await supabase.from("beatmapPages").delete().eq("id", id);
73
+ await supabase
74
+ .from("beatmaps")
75
+ .delete()
76
+ .eq("beatmapHash", beatmapData.beatmapHash);
77
+
78
+ await invalidateCache(`beatmap-comments:${id}`);
79
+ if (pageData.latestBeatmapHash) {
80
+ await invalidateCachePrefix(`beatmap-scores:${pageData.latestBeatmapHash}`);
81
+ }
82
+
83
+ return NextResponse.json({});
84
+ }
@@ -1,11 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { Database } from "../types/database";
4
- import { protectedApi, validUser } from "../utils/requestUtils";
5
- import { supabase } from "../utils/supabase";
6
- import { getUserBySession } from "../utils/getUserBySession";
7
- import { User } from "@supabase/supabase-js";
8
- import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
2
+ import z from "zod";
3
+ import { Database } from "../types/database";
4
+ import { protectedApi, validUser } from "../utils/requestUtils";
5
+ import { supabase } from "../utils/supabase";
6
+ import { getUserBySession } from "../utils/getUserBySession";
7
+ import { User } from "@supabase/supabase-js";
8
+ import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
9
9
 
10
10
  // Define supported admin operations and their parameter types
11
11
  const adminOperations = {
@@ -146,21 +146,21 @@ export async function handler(
146
146
  { status: 403 }
147
147
  );
148
148
  }
149
-
150
- // Execute the requested admin operation
151
- try {
152
- let result: { data?: any; error?: any } | null = null;
153
- const operation = data.data.operation;
154
- const params = data.data.params as any;
155
- const targetUserId =
156
- "userId" in params && typeof params.userId === "number"
157
- ? params.userId
158
- : null;
159
-
160
- switch (operation) {
161
- case "deleteUser":
162
- result = await supabase.rpc("admin_delete_user", {
163
- user_id: params.userId,
149
+
150
+ // Execute the requested admin operation
151
+ try {
152
+ let result: { data?: any; error?: any } | null = null;
153
+ const operation = data.data.operation;
154
+ const params = data.data.params as any;
155
+ const targetUserId =
156
+ "userId" in params && typeof params.userId === "number"
157
+ ? params.userId
158
+ : null;
159
+
160
+ switch (operation) {
161
+ case "deleteUser":
162
+ result = await supabase.rpc("admin_delete_user", {
163
+ user_id: params.userId,
164
164
  });
165
165
  break;
166
166
 
@@ -221,23 +221,23 @@ export async function handler(
221
221
  })
222
222
  .select();
223
223
  break;
224
- case "changeBadges":
225
- // Allow only developers to modify badges.
226
- if ((queryUserData.badges as string[]).includes("Developer")) {
227
- result = await supabase
228
- .from("profiles")
229
- .upsert({
230
- id: params.userId,
231
- badges: JSON.parse(params.badges),
232
- })
233
- .select();
234
- } else {
235
- result = { data: null, error: { message: "Unauthorized" } };
236
- }
237
- break;
238
-
239
- case "addBadge":
240
- // Allow only developers to modify badges.
224
+ case "changeBadges":
225
+ // Allow only developers to modify badges.
226
+ if ((queryUserData.badges as string[]).includes("Developer")) {
227
+ result = await supabase
228
+ .from("profiles")
229
+ .upsert({
230
+ id: params.userId,
231
+ badges: JSON.parse(params.badges),
232
+ })
233
+ .select();
234
+ } else {
235
+ result = { data: null, error: { message: "Unauthorized" } };
236
+ }
237
+ break;
238
+
239
+ case "addBadge":
240
+ // Allow only developers to modify badges.
241
241
  if ((queryUserData.badges as string[]).includes("Developer")) {
242
242
  // Get current badges
243
243
  const { data: targetUser } = await supabase
@@ -249,23 +249,23 @@ export async function handler(
249
249
  const currentBadges = (targetUser?.badges || []) as string[];
250
250
  if (!currentBadges.includes(params.badge)) {
251
251
  currentBadges.push(params.badge);
252
- result = await supabase
253
- .from("profiles")
254
- .upsert({
255
- id: params.userId,
256
- badges: currentBadges,
257
- })
258
- .select();
259
- } else {
260
- result = { data: targetUser, error: null };
261
- }
262
- } else {
263
- result = { data: null, error: { message: "Unauthorized" } };
264
- }
265
- break;
266
-
267
- case "removeBadge":
268
- // Allow only developers to modify badges.
252
+ result = await supabase
253
+ .from("profiles")
254
+ .upsert({
255
+ id: params.userId,
256
+ badges: currentBadges,
257
+ })
258
+ .select();
259
+ } else {
260
+ result = { data: targetUser, error: null };
261
+ }
262
+ } else {
263
+ result = { data: null, error: { message: "Unauthorized" } };
264
+ }
265
+ break;
266
+
267
+ case "removeBadge":
268
+ // Allow only developers to modify badges.
269
269
  if ((queryUserData.badges as string[]).includes("Developer")) {
270
270
  // Get current badges
271
271
  const { data: targetUser } = await supabase
@@ -277,17 +277,17 @@ export async function handler(
277
277
  const currentBadges = (targetUser?.badges || []) as string[];
278
278
  const updatedBadges = currentBadges.filter(b => b !== params.badge);
279
279
 
280
- result = await supabase
281
- .from("profiles")
282
- .upsert({
283
- id: params.userId,
284
- badges: updatedBadges,
285
- })
286
- .select();
287
- } else {
288
- result = { data: null, error: { message: "Unauthorized" } };
289
- }
290
- break;
280
+ result = await supabase
281
+ .from("profiles")
282
+ .upsert({
283
+ id: params.userId,
284
+ badges: updatedBadges,
285
+ })
286
+ .select();
287
+ } else {
288
+ result = { data: null, error: { message: "Unauthorized" } };
289
+ }
290
+ break;
291
291
 
292
292
  case "getScoresPaginated":
293
293
  const offset = (params.page - 1) * params.limit;
@@ -349,52 +349,52 @@ export async function handler(
349
349
  }
350
350
 
351
351
  // Log the admin action
352
- await supabase.rpc("admin_log_action", {
353
- admin_id: queryUserData.id,
354
- action_type: operation,
355
- target_id: "userId" in params ? params.userId : null,
356
- details: { params },
357
- });
358
-
359
- if (result?.error) {
360
- return NextResponse.json(
361
- {
362
- success: false,
363
- error: result.error.message,
364
- },
365
- { status: 500 }
366
- );
367
- }
368
-
369
- if (targetUserId !== null && !result?.error) {
370
- await invalidateCachePrefix(`userscore:${targetUserId}`);
371
-
372
- if (
373
- operation === "removeAllScores" ||
374
- operation === "invalidateRankedScores" ||
375
- operation === "deleteUser"
376
- ) {
377
- const { data: beatmapHashes } = await supabase
378
- .from("scores")
379
- .select("beatmapHash")
380
- .eq("userId", targetUserId);
381
-
382
- const uniqueHashes = new Set(
383
- (beatmapHashes || [])
384
- .map((row) => row.beatmapHash)
385
- .filter((hash): hash is string => Boolean(hash))
386
- );
387
-
388
- for (const hash of uniqueHashes) {
389
- await invalidateCachePrefix(`beatmap-scores:${hash}`);
390
- }
391
- }
392
- }
393
-
394
- return NextResponse.json({
395
- success: true,
396
- result: result?.data,
397
- });
352
+ await supabase.rpc("admin_log_action", {
353
+ admin_id: queryUserData.id,
354
+ action_type: operation,
355
+ target_id: "userId" in params ? params.userId : null,
356
+ details: { params },
357
+ });
358
+
359
+ if (result?.error) {
360
+ return NextResponse.json(
361
+ {
362
+ success: false,
363
+ error: result.error.message,
364
+ },
365
+ { status: 500 }
366
+ );
367
+ }
368
+
369
+ if (targetUserId !== null && !result?.error) {
370
+ await invalidateCachePrefix(`userscore:${targetUserId}`);
371
+
372
+ if (
373
+ operation === "removeAllScores" ||
374
+ operation === "invalidateRankedScores" ||
375
+ operation === "deleteUser"
376
+ ) {
377
+ const { data: beatmapHashes } = await supabase
378
+ .from("scores")
379
+ .select("beatmapHash")
380
+ .eq("userId", targetUserId);
381
+
382
+ const uniqueHashes = new Set(
383
+ (beatmapHashes || [])
384
+ .map((row) => row.beatmapHash)
385
+ .filter((hash): hash is string => Boolean(hash))
386
+ );
387
+
388
+ for (const hash of uniqueHashes) {
389
+ await invalidateCachePrefix(`beatmap-scores:${hash}`);
390
+ }
391
+ }
392
+ }
393
+
394
+ return NextResponse.json({
395
+ success: true,
396
+ result: result?.data,
397
+ });
398
398
  } catch (err: any) {
399
399
  return NextResponse.json(
400
400
  {
@@ -1,10 +1,12 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { supabase } from "../utils/supabase";
4
+ import { getActiveProfileIdSet } from "../utils/activityStatus";
4
5
 
5
6
  export const Schema = {
6
7
  input: z.strictObject({
7
8
  limit: z.number().min(1).max(100).optional().default(100),
9
+ include_inactive: z.boolean().optional().default(false),
8
10
  }),
9
11
  output: z.object({
10
12
  leaderboard: z.array(
@@ -21,13 +23,20 @@ export const Schema = {
21
23
  };
22
24
 
23
25
  export async function POST(request: Request): Promise<NextResponse> {
24
- return handler({ limit: 100 });
26
+ let body: unknown = {};
27
+ try {
28
+ body = await request.json();
29
+ } catch {}
30
+
31
+ return handler(Schema.input.parse(body));
25
32
  }
26
33
 
27
34
  export async function handler({
28
35
  limit = 100,
36
+ include_inactive = false,
29
37
  }: {
30
38
  limit?: number;
39
+ include_inactive?: boolean;
31
40
  }): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
32
41
  try {
33
42
  const { data: leaderboard, error } = await supabase.rpc(
@@ -49,9 +58,22 @@ export async function handler({
49
58
  );
50
59
  }
51
60
 
61
+ const entries = leaderboard || [];
62
+
63
+ if (include_inactive) {
64
+ return NextResponse.json({
65
+ leaderboard: entries,
66
+ total_count: entries.length,
67
+ });
68
+ }
69
+
70
+ const activeIds = await getActiveProfileIdSet(entries.map((entry) => entry.id));
71
+
72
+ const filteredLeaderboard = entries.filter((entry) => activeIds.has(entry.id));
73
+
52
74
  return NextResponse.json({
53
- leaderboard: leaderboard || [],
54
- total_count: leaderboard?.length || 0,
75
+ leaderboard: filteredLeaderboard,
76
+ total_count: filteredLeaderboard.length,
55
77
  });
56
78
  } catch (error) {
57
79
  console.error("Badge leaderboard exception:", error);
@@ -40,7 +40,8 @@ export async function handler(
40
40
  export async function getLeaderboard(badge: string) {
41
41
  let { data: queryData, error } = await supabase
42
42
  .from("profiles")
43
- .select("flag,id,username,badges");
43
+ .select("flag,id,username,badges,scores!inner(id)")
44
+ .limit(1, { foreignTable: "scores" });
44
45
 
45
46
  const users = queryData?.filter((e) =>
46
47
  ((e.badges || []) as string[]).includes(badge)
@@ -1,8 +1,8 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { protectedApi, validUser } from "../utils/requestUtils";
4
- import { getCacheValue, setCacheValue } from "../utils/cache";
5
- import { supabase } from "../utils/supabase";
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { getCacheValue, setCacheValue } from "../utils/cache";
5
+ import { supabase } from "../utils/supabase";
6
6
 
7
7
  export const Schema = {
8
8
  input: z.strictObject({
@@ -35,25 +35,25 @@ export async function POST(request: Request): Promise<NextResponse> {
35
35
  });
36
36
  }
37
37
 
38
- export async function handler({
39
- page,
40
- }: (typeof Schema)["input"]["_type"]): Promise<
41
- NextResponse<(typeof Schema)["output"]["_type"]>
42
- > {
43
- const cacheKey = `beatmap-comments:${page}`;
44
- const cachedComments = await getCacheValue<
45
- (typeof Schema)["output"]["_type"]["comments"]
46
- >(cacheKey);
47
-
48
- if (cachedComments) {
49
- return NextResponse.json({ comments: cachedComments });
50
- }
51
-
52
- let { data: userData, error: userError } = await supabase
53
- .from("beatmapPageComments")
54
- .select(
55
- `
56
- *,
38
+ export async function handler({
39
+ page,
40
+ }: (typeof Schema)["input"]["_type"]): Promise<
41
+ NextResponse<(typeof Schema)["output"]["_type"]>
42
+ > {
43
+ const cacheKey = `beatmap-comments:${page}`;
44
+ const cachedComments = await getCacheValue<
45
+ (typeof Schema)["output"]["_type"]["comments"]
46
+ >(cacheKey);
47
+
48
+ if (cachedComments) {
49
+ return NextResponse.json({ comments: cachedComments });
50
+ }
51
+
52
+ let { data: userData, error: userError } = await supabase
53
+ .from("beatmapPageComments")
54
+ .select(
55
+ `
56
+ *,
57
57
  profiles!inner(
58
58
  username,
59
59
  avatar_url,
@@ -61,11 +61,11 @@ export async function handler({
61
61
  )
62
62
  `
63
63
  )
64
- .eq("beatmapPage", page);
65
-
66
- if (userData) {
67
- await setCacheValue(cacheKey, userData);
68
- }
69
-
70
- return NextResponse.json({ comments: userData! });
71
- }
64
+ .eq("beatmapPage", page);
65
+
66
+ if (userData) {
67
+ await setCacheValue(cacheKey, userData);
68
+ }
69
+
70
+ return NextResponse.json({ comments: userData! });
71
+ }
@@ -1,19 +1,19 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { protectedApi } from "../utils/requestUtils";
4
- import { getCacheValue, setCacheValue } from "../utils/cache";
5
- import { supabase } from "../utils/supabase";
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi } from "../utils/requestUtils";
4
+ import { getCacheValue, setCacheValue } from "../utils/cache";
5
+ import { supabase } from "../utils/supabase";
6
6
 
7
- export const Schema = {
8
- input: z.strictObject({
9
- session: z.string(),
10
- id: z.number(),
11
- limit: z.number().min(1).max(200).default(50),
12
- }),
13
- output: z.object({
14
- error: z.string().optional(),
15
- scores: z
16
- .array(
7
+ export const Schema = {
8
+ input: z.strictObject({
9
+ session: z.string(),
10
+ id: z.number(),
11
+ limit: z.number().min(1).max(200).default(50),
12
+ }),
13
+ output: z.object({
14
+ error: z.string().optional(),
15
+ scores: z
16
+ .array(
17
17
  z.object({
18
18
  id: z.number(),
19
19
  awarded_sp: z.number().nullable(),
@@ -68,17 +68,17 @@ export async function POST(request: Request): Promise<NextResponse> {
68
68
  });
69
69
  }
70
70
 
71
- export async function handler(
72
- data: (typeof Schema)["input"]["_type"],
73
- req: Request
74
- ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
75
- const limit = data.limit ?? 50;
76
-
77
- let { data: beatmapPage, error: errorlast } = await supabase
78
- .from("beatmapPages")
79
- .select(
80
- `
81
- *,
71
+ export async function handler(
72
+ data: (typeof Schema)["input"]["_type"],
73
+ req: Request
74
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
75
+ const limit = data.limit ?? 50;
76
+
77
+ let { data: beatmapPage, error: errorlast } = await supabase
78
+ .from("beatmapPages")
79
+ .select(
80
+ `
81
+ *,
82
82
  beatmaps (
83
83
  created_at,
84
84
  playcount,
@@ -97,47 +97,47 @@ export async function handler(
97
97
  avatar_url
98
98
  )
99
99
  `
100
- )
101
- .eq("id", data.id)
102
- .single();
103
-
104
- if (!beatmapPage) return NextResponse.json({});
105
-
106
- const beatmapHash = beatmapPage?.latestBeatmapHash || "";
107
- const isCacheable =
108
- beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
109
- const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
110
-
111
- let scoreData: any[] | null = null;
112
-
113
- if (isCacheable && beatmapHash) {
114
- scoreData = await getCacheValue<any[]>(cacheKey);
115
- }
116
-
117
- if (!scoreData) {
118
- const { data: rpcScores, error } = await supabase.rpc(
119
- "get_top_scores_for_beatmap",
120
- { beatmap_hash: beatmapHash }
121
- );
122
-
123
- if (error) {
124
- return NextResponse.json({ error: JSON.stringify(error) });
125
- }
126
-
127
- scoreData = (rpcScores || []).slice(0, limit);
128
-
129
- if (isCacheable && beatmapHash) {
130
- await setCacheValue(cacheKey, scoreData);
131
- }
132
- }
133
-
134
- return NextResponse.json({
135
- scores: (scoreData || []).map((score: any) => ({
136
- id: score.id,
137
- awarded_sp: score.awarded_sp,
138
- created_at: score.created_at,
139
- misses: score.misses,
140
- mods: score.mods,
100
+ )
101
+ .eq("id", data.id)
102
+ .single();
103
+
104
+ if (!beatmapPage) return NextResponse.json({});
105
+
106
+ const beatmapHash = beatmapPage?.latestBeatmapHash || "";
107
+ const isCacheable =
108
+ beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
109
+ const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
110
+
111
+ // let scoreData: any[] | null = null;
112
+
113
+ // if (isCacheable && beatmapHash) {
114
+ // scoreData = await getCacheValue<any[]>(cacheKey);
115
+ // }
116
+
117
+ // if (!scoreData) {
118
+ // const { data: rpcScores, error } = await supabase.rpc(
119
+ // "get_top_scores_for_beatmap",
120
+ // { beatmap_hash: beatmapHash }
121
+ // );
122
+
123
+ // if (error) {
124
+ // return NextResponse.json({ error: JSON.stringify(error) });
125
+ // }
126
+
127
+ // scoreData = (rpcScores || []).slice(0, limit);
128
+
129
+ // if (isCacheable && beatmapHash) {
130
+ // await setCacheValue(cacheKey, scoreData);
131
+ // }
132
+ // }
133
+
134
+ return NextResponse.json({
135
+ scores: [].map((score: any) => ({
136
+ id: score.id,
137
+ awarded_sp: score.awarded_sp,
138
+ created_at: score.created_at,
139
+ misses: score.misses,
140
+ mods: score.mods,
141
141
  passed: score.passed,
142
142
  songId: score.songid,
143
143
  speed: score.speed,