rhythia-api 131.0.0 → 132.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/api/approveMap.ts CHANGED
@@ -56,7 +56,7 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
56
56
 
57
57
  if ((mapData.nominations as number[])!.length < 2) {
58
58
  return NextResponse.json({
59
- error: "Maps can get approved only if they have 2 approvals",
59
+ error: "Maps can get approved only if they have 2 nominations",
60
60
  });
61
61
  }
62
62
 
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ page: z.number(),
9
+ }),
10
+ output: z.strictObject({
11
+ error: z.string().optional(),
12
+ comments: z.array(
13
+ z.object({
14
+ beatmapPage: z.number(),
15
+ content: z.string().nullable(),
16
+ owner: z.number(),
17
+ })
18
+ ),
19
+ }),
20
+ };
21
+
22
+ export async function POST(request: Request): Promise<NextResponse> {
23
+ return protectedApi({
24
+ request,
25
+ schema: Schema,
26
+ authorization: () => {},
27
+ activity: handler,
28
+ });
29
+ }
30
+
31
+ export async function handler({
32
+ page,
33
+ }: (typeof Schema)["input"]["_type"]): Promise<
34
+ NextResponse<(typeof Schema)["output"]["_type"]>
35
+ > {
36
+ let { data: userData, error: userError } = await supabase
37
+ .from("beatmapPageComments")
38
+ .select("*")
39
+ .eq("beatmapPage", page);
40
+
41
+ return NextResponse.json({ comments: userData! });
42
+ }
@@ -0,0 +1,56 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ session: z.string(),
9
+ page: z.number(),
10
+ content: z.string(),
11
+ }),
12
+ output: z.strictObject({
13
+ error: z.string().optional(),
14
+ }),
15
+ };
16
+
17
+ export async function POST(request: Request): Promise<NextResponse> {
18
+ return protectedApi({
19
+ request,
20
+ schema: Schema,
21
+ authorization: validUser,
22
+ activity: handler,
23
+ });
24
+ }
25
+
26
+ export async function handler({
27
+ session,
28
+ page,
29
+ content,
30
+ }: (typeof Schema)["input"]["_type"]): Promise<
31
+ NextResponse<(typeof Schema)["output"]["_type"]>
32
+ > {
33
+ const user = (await supabase.auth.getUser(session)).data.user!;
34
+ let { data: userData, error: userError } = await supabase
35
+ .from("profiles")
36
+ .select("*")
37
+ .eq("uid", user.id)
38
+ .single();
39
+
40
+ if (!userData) return NextResponse.json({ error: "No user." });
41
+
42
+ const upserted = await supabase
43
+ .from("beatmapPageComments")
44
+ .upsert({
45
+ beatmapPage: page,
46
+ owner: userData.id,
47
+ content,
48
+ })
49
+ .select("*")
50
+ .single();
51
+
52
+ if (upserted.error?.message.length) {
53
+ return NextResponse.json({ error: upserted.error.message });
54
+ }
55
+ return NextResponse.json({});
56
+ }
package/index.ts CHANGED
@@ -35,6 +35,11 @@ import { Schema as GetBadgedUsers } from "./api/getBadgedUsers"
35
35
  export { Schema as SchemaGetBadgedUsers } from "./api/getBadgedUsers"
36
36
  export const getBadgedUsers = handleApi({url:"/api/getBadgedUsers",...GetBadgedUsers})
37
37
 
38
+ // ./api/getBeatmapComments.ts API
39
+ import { Schema as GetBeatmapComments } from "./api/getBeatmapComments"
40
+ export { Schema as SchemaGetBeatmapComments } from "./api/getBeatmapComments"
41
+ export const getBeatmapComments = handleApi({url:"/api/getBeatmapComments",...GetBeatmapComments})
42
+
38
43
  // ./api/getBeatmapPage.ts API
39
44
  import { Schema as GetBeatmapPage } from "./api/getBeatmapPage"
40
45
  export { Schema as SchemaGetBeatmapPage } from "./api/getBeatmapPage"
@@ -80,6 +85,11 @@ import { Schema as NominateMap } from "./api/nominateMap"
80
85
  export { Schema as SchemaNominateMap } from "./api/nominateMap"
81
86
  export const nominateMap = handleApi({url:"/api/nominateMap",...NominateMap})
82
87
 
88
+ // ./api/postBeatmapComment.ts API
89
+ import { Schema as PostBeatmapComment } from "./api/postBeatmapComment"
90
+ export { Schema as SchemaPostBeatmapComment } from "./api/postBeatmapComment"
91
+ export const postBeatmapComment = handleApi({url:"/api/postBeatmapComment",...PostBeatmapComment})
92
+
83
93
  // ./api/rankMapsArchive.ts API
84
94
  import { Schema as RankMapsArchive } from "./api/rankMapsArchive"
85
95
  export { Schema as SchemaRankMapsArchive } from "./api/rankMapsArchive"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "131.0.0",
3
+ "version": "132.0.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "update": "bun ./scripts/update.ts",
@@ -32,6 +32,7 @@
32
32
  "isomorphic-git": "^1.27.1",
33
33
  "lodash": "^4.17.21",
34
34
  "next": "^14.2.5",
35
+ "osu-classes": "^3.1.0",
35
36
  "osu-parsers": "^4.1.7",
36
37
  "osu-standard-stable": "^5.0.0",
37
38
  "sharp": "^0.33.5",
package/scripts/test.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/database";
3
+ import { SSPMParser } from "../utils/star-calc/sspmParser";
4
+ import { rateMap, rateMapOld } from "../utils/star-calc";
5
+ import { V1SSPMParser } from "../utils/star-calc/sspmv1Parser";
3
6
 
4
7
  export const supabase = createClient<Database>(
5
8
  `https://pfkajngbllcbdzoylrvp.supabase.co`,
@@ -13,24 +16,52 @@ export const supabase = createClient<Database>(
13
16
  );
14
17
 
15
18
  async function main() {
16
- const { data: mapData, error } = await supabase
17
- .from("beatmapPages")
18
- .select("id,nominations,owner,status")
19
- .eq("owner", 10)
20
- .eq("status", "RANKED");
21
-
22
- if (!mapData) {
23
- throw "lolz";
24
- }
19
+ const req = await fetch(`https://cdn.rhythia.net/index.json`);
20
+ const json = Object.values(await req.json()) as any[];
21
+ console.log(json[0]);
22
+
23
+ let count = 0;
24
+ for (const map of json) {
25
+ const request = await fetch(map.download);
26
+ const bytes = await request.arrayBuffer();
27
+ const parser = new SSPMParser(Buffer.from(bytes));
28
+ let rate = 0;
29
+ try {
30
+ const data = parser.parse();
31
+ rate = rateMap(data);
32
+ } catch (error) {
33
+ const parserOld = new V1SSPMParser(Buffer.from(bytes));
34
+ const data = parserOld.parse();
25
35
 
26
- for (const element of mapData) {
27
- let data = await supabase.from("beatmapPages").upsert({
28
- id: element.id,
29
- nominations: [10, 10],
30
- status: "APPROVED",
36
+ rate = rateMapOld(data);
37
+ }
38
+ await supabase.from("beatmaps").upsert({
39
+ beatmapHash: map.id,
40
+ starRating: rate,
31
41
  });
32
- console.log(data.error);
42
+ count++;
43
+ console.log(count, json.length, map.id, rate);
33
44
  }
45
+ // for (const map of json) {
46
+ // const reqMap = await fetch(map.link);
47
+ // const buff = await reqMap.arrayBuffer();
48
+ // writeFileSync("./toSubmit/" + map.id + ".sspm", Buffer.from(buff));
49
+ // }
50
+
51
+ // const { data: mapData, error } = await supabase
52
+ // .from("beatmapPages")
53
+ // .select("id,nominations,owner,status")
54
+ // .eq("owner", 10)
55
+ // .eq("status", "RANKED");
56
+
57
+ // if (!mapData) {
58
+ // throw "lolz";
59
+ // }
60
+
61
+ // for (const element of mapData) {
62
+
63
+ // console.log(data.error);
64
+ // }
34
65
  }
35
66
 
36
67
  main();
package/types/database.ts CHANGED
@@ -9,6 +9,45 @@ export type Json =
9
9
  export type Database = {
10
10
  public: {
11
11
  Tables: {
12
+ beatmapPageComments: {
13
+ Row: {
14
+ beatmapPage: number
15
+ content: string | null
16
+ created_at: string
17
+ id: number
18
+ owner: number
19
+ }
20
+ Insert: {
21
+ beatmapPage: number
22
+ content?: string | null
23
+ created_at?: string
24
+ id?: number
25
+ owner: number
26
+ }
27
+ Update: {
28
+ beatmapPage?: number
29
+ content?: string | null
30
+ created_at?: string
31
+ id?: number
32
+ owner?: number
33
+ }
34
+ Relationships: [
35
+ {
36
+ foreignKeyName: "beatmapPageComments_beatmapPage_fkey"
37
+ columns: ["beatmapPage"]
38
+ isOneToOne: false
39
+ referencedRelation: "beatmapPages"
40
+ referencedColumns: ["id"]
41
+ },
42
+ {
43
+ foreignKeyName: "beatmapPageComments_owner_fkey"
44
+ columns: ["owner"]
45
+ isOneToOne: false
46
+ referencedRelation: "profiles"
47
+ referencedColumns: ["id"]
48
+ },
49
+ ]
50
+ }
12
51
  beatmapPages: {
13
52
  Row: {
14
53
  created_at: string
@@ -2,6 +2,7 @@ import { BeatmapDecoder } from "osu-parsers";
2
2
  import { StandardRuleset } from "osu-standard-stable";
3
3
  import { sampleMap } from "./osuUtils";
4
4
  import { SSPMParsedMap } from "./sspmParser";
5
+ import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
5
6
 
6
7
  function easeInExpoDeq(x: number) {
7
8
  return x === 0 ? 0 : Math.pow(2, 35 * x - 35);
@@ -20,7 +21,7 @@ export function rateMap(map: SSPMParsedMap) {
20
21
  const decoder = new BeatmapDecoder();
21
22
  const beatmap1 = decoder.decodeFromString(sampleMap);
22
23
 
23
- const notes = map.markers
24
+ let notes = map.markers
24
25
  .filter((marker) => marker.type === 0)
25
26
  .map((marker) => ({
26
27
  time: marker.position,
@@ -28,6 +29,34 @@ export function rateMap(map: SSPMParsedMap) {
28
29
  y: marker.data["field0"].y,
29
30
  }));
30
31
 
32
+ notes = notes.sort((a, b) => a.time - b.time);
33
+
34
+ for (const note of notes) {
35
+ const hittable = beatmap1.hitObjects[0].clone();
36
+ hittable.startX = Math.round((note.x / 2) * 100);
37
+ hittable.startY = Math.round((note.y / 2) * 100);
38
+ hittable.startTime = note.time;
39
+ beatmap1.hitObjects.push(hittable);
40
+ }
41
+ const ruleset = new StandardRuleset();
42
+ const mods = ruleset.createModCombination("RX");
43
+ const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
44
+ const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
45
+ return difficultyAttributes.starRating;
46
+ }
47
+
48
+ export function rateMapOld(map: SSPMMap) {
49
+ const decoder = new BeatmapDecoder();
50
+ const beatmap1 = decoder.decodeFromString(sampleMap);
51
+
52
+ let notes = map.notes.map((marker) => ({
53
+ time: marker.position,
54
+ x: marker.x,
55
+ y: marker.y,
56
+ }));
57
+
58
+ notes = notes.sort((a, b) => a.time - b.time);
59
+
31
60
  for (const note of notes) {
32
61
  const hittable = beatmap1.hitObjects[0].clone();
33
62
  hittable.startX = Math.round((note.x / 2) * 100);
@@ -233,6 +233,9 @@ export class SSPMParser {
233
233
  version: this.readUInt16(),
234
234
  reserved: this.readBytes(4),
235
235
  };
236
+ if (header.version == 1) {
237
+ throw "Can't parse";
238
+ }
236
239
 
237
240
  // Static Metadata
238
241
  const metadata: StaticMetadata = {
@@ -278,7 +281,8 @@ export class SSPMParser {
278
281
  mapID: this.readString(),
279
282
  mapName: this.readString(),
280
283
  songName: this.readString(),
281
- mappers: this.readStringList(this.readUInt16()),
284
+ mappers: [],
285
+ // mappers: this.readStringList(this.readUInt16()),
282
286
  };
283
287
 
284
288
  let customData: CustomData = { fields: [] };
@@ -0,0 +1,165 @@
1
+ enum Difficulty {
2
+ NA = 0x00,
3
+ Easy = 0x01,
4
+ Medium = 0x02,
5
+ Hard = 0x03,
6
+ Logic = 0x04,
7
+ Tasukete = 0x05,
8
+ }
9
+
10
+ enum CoverStorageType {
11
+ None = 0x00,
12
+ PNG = 0x02,
13
+ }
14
+
15
+ enum AudioStorageType {
16
+ None = 0x00,
17
+ StoredAudioFile = 0x01,
18
+ }
19
+
20
+ enum NoteStorageType {
21
+ Integer = 0x00,
22
+ Quantum = 0x01,
23
+ }
24
+
25
+ interface Note {
26
+ position: number;
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ export interface SSPMMap {
32
+ id: string;
33
+ name: string;
34
+ creator: string;
35
+ lastNotePosition: number;
36
+ noteCount: number;
37
+ difficulty: Difficulty;
38
+ cover: Buffer | null;
39
+ audio: Buffer | null;
40
+ notes: Note[];
41
+ }
42
+
43
+ export class V1SSPMParser {
44
+ private buffer: Buffer;
45
+ private offset: number = 0;
46
+
47
+ constructor(buffer: Buffer) {
48
+ this.buffer = buffer;
49
+ }
50
+
51
+ parse(): SSPMMap {
52
+ this.validateHeader();
53
+ const metadata = this.parseMetadata();
54
+ const cover = this.parseCover();
55
+ const audio = this.parseAudio();
56
+ const notes = this.parseNotes(metadata.noteCount);
57
+
58
+ return {
59
+ ...metadata,
60
+ cover,
61
+ audio,
62
+ notes,
63
+ };
64
+ }
65
+
66
+ private validateHeader() {
67
+ const signature = this.buffer.slice(0, 4).toString("hex");
68
+ if (signature !== "53532b6d") {
69
+ throw new Error("Invalid SSPM file signature");
70
+ }
71
+
72
+ const version = this.buffer.readUInt16LE(4);
73
+ if (version !== 1) {
74
+ throw new Error(`Unsupported SSPM version: ${version}`);
75
+ }
76
+
77
+ this.offset = 8; // Skip reserved space
78
+ }
79
+
80
+ private parseMetadata(): Omit<SSPMMap, "cover" | "audio" | "notes"> {
81
+ const id = this.readString();
82
+ const name = this.readString();
83
+ const creator = this.readString();
84
+ const lastNotePosition = this.buffer.readUInt32LE(this.offset);
85
+ this.offset += 4;
86
+ const noteCount = this.buffer.readUInt32LE(this.offset);
87
+ this.offset += 4;
88
+ const difficulty = this.buffer.readUInt8(this.offset++) as Difficulty;
89
+
90
+ return { id, name, creator, lastNotePosition, noteCount, difficulty };
91
+ }
92
+
93
+ private parseCover(): Buffer | null {
94
+ const coverType = this.buffer.readUInt8(this.offset++) as CoverStorageType;
95
+ if (coverType === CoverStorageType.None) {
96
+ return null;
97
+ } else if (coverType === CoverStorageType.PNG) {
98
+ const length = this.buffer.readBigUInt64LE(this.offset);
99
+ this.offset += 8;
100
+ const cover = this.buffer.slice(
101
+ this.offset,
102
+ this.offset + Number(length)
103
+ );
104
+ this.offset += Number(length);
105
+ return cover;
106
+ } else {
107
+ throw new Error(`Unsupported cover storage type: ${coverType}`);
108
+ }
109
+ }
110
+
111
+ private parseAudio(): Buffer | null {
112
+ const audioType = this.buffer.readUInt8(this.offset++) as AudioStorageType;
113
+ if (audioType === AudioStorageType.None) {
114
+ return null;
115
+ } else if (audioType === AudioStorageType.StoredAudioFile) {
116
+ const length = this.buffer.readBigUInt64LE(this.offset);
117
+ this.offset += 8;
118
+ const audio = this.buffer.slice(
119
+ this.offset,
120
+ this.offset + Number(length)
121
+ );
122
+ this.offset += Number(length);
123
+ return audio;
124
+ } else {
125
+ throw new Error(`Unsupported audio storage type: ${audioType}`);
126
+ }
127
+ }
128
+
129
+ private parseNotes(count: number): Note[] {
130
+ const notes: Note[] = [];
131
+ for (let i = 0; i < count; i++) {
132
+ const position = this.buffer.readUInt32LE(this.offset);
133
+ this.offset += 4;
134
+ const storageType = this.buffer.readUInt8(
135
+ this.offset++
136
+ ) as NoteStorageType;
137
+
138
+ let x: number, y: number;
139
+ if (storageType === NoteStorageType.Integer) {
140
+ x = this.buffer.readUInt8(this.offset++);
141
+ y = this.buffer.readUInt8(this.offset++);
142
+ } else if (storageType === NoteStorageType.Quantum) {
143
+ x = this.buffer.readFloatLE(this.offset);
144
+ this.offset += 4;
145
+ y = this.buffer.readFloatLE(this.offset);
146
+ this.offset += 4;
147
+ } else {
148
+ throw new Error(`Unsupported note storage type: ${storageType}`);
149
+ }
150
+
151
+ notes.push({ position, x, y });
152
+ }
153
+ return notes;
154
+ }
155
+
156
+ private readString(): string {
157
+ let end = this.offset;
158
+ while (this.buffer[end] !== 0x0a) {
159
+ end++;
160
+ }
161
+ const str = this.buffer.slice(this.offset, end).toString("utf-8");
162
+ this.offset = end + 1;
163
+ return str;
164
+ }
165
+ }