Skip to content

Commit

Permalink
Clean code, split getUserScores in two
Browse files Browse the repository at this point in the history
This means some changes to Score stuff for type safety sake
A beatmap's source can no longer be 0
getURL's functions now support using a custom server
  • Loading branch information
TTTaevas committed Nov 27, 2023
1 parent 23e4a53 commit a3bfe5b
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 122 deletions.
77 changes: 37 additions & 40 deletions lib/beatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,54 @@
* For the `approved` of a `Beatmap` (for example, `Categories[beatmap.approved]` would return "RANKED" if 1) https://osu.ppy.sh/wiki/en/Beatmap/Category
*/
export enum Categories {
GRAVEYARD = -2,
WIP = -1,
PENDING = 0,
RANKED = 1,
APPROVED = 2,
QUALIFIED = 3,
GRAVEYARD = -2,
WIP = -1,
PENDING = 0,
RANKED = 1,
APPROVED = 2,
QUALIFIED = 3,
}

/**
* For the `genre_id` of a `Beatmap` (for example, `Genres[beatmap.genre_id]` would return "NOVELTY" if 7)
*/
export enum Genres {
ANY = 0,
UNSPECIFIED = 1,
"VIDEO GAME" = 2,
ANIME = 3,
ROCK = 4,
POP = 5,
OTHER = 6,
NOVELTY = 7,
"" = 8,
"HIP HOP" = 9,
ELECTRONIC = 10,
METAL = 11,
CLASSICAL = 12,
FOLK = 13,
JAZZ = 14,
ANY = 0,
UNSPECIFIED = 1,
"VIDEO GAME" = 2,
ANIME = 3,
ROCK = 4,
POP = 5,
OTHER = 6,
NOVELTY = 7,
"" = 8,
"HIP HOP" = 9,
ELECTRONIC = 10,
METAL = 11,
CLASSICAL = 12,
FOLK = 13,
JAZZ = 14,
}

/**
* For the `language_id` of a `Beatmap` (for example, `Languages[beatmap.language_id]` would return "FRENCH" if 7)
*/
export enum Languages {
ANY = 0,
UNSPECIFIED = 1,
ENGLISH = 2,
JAPANESE = 3,
CHINESE = 4,
INSTRUMENTAL = 5,
KOREAN = 6,
FRENCH = 7,
GERMAN = 8,
SWEDISH = 9,
SPANISH = 10,
ITALIAN = 11,
RUSSIAN = 12,
POLISH = 13,
OTHER = 14,
ANY = 0,
UNSPECIFIED = 1,
ENGLISH = 2,
JAPANESE = 3,
CHINESE = 4,
INSTRUMENTAL = 5,
KOREAN = 6,
FRENCH = 7,
GERMAN = 8,
SWEDISH = 9,
SPANISH = 10,
ITALIAN = 11,
RUSSIAN = 12,
POLISH = 13,
OTHER = 14,
}

export interface Beatmap {
Expand Down Expand Up @@ -120,10 +120,7 @@ export interface Beatmap {
creator: string
creator_id: number
bpm: number
/**
* Is 0 if no source
*/
source: string | 0
source: string
tags: string
genre_id: number
language_id: number
Expand Down
80 changes: 43 additions & 37 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import fetch, { FetchError } from "node-fetch"
import { User } from "./user.js"
import { Score, ScoreWithBeatmapid } from "./score.js"
import { Score, ScoreWithBeatmapid, ScoreWithBeatmapidReplayavailablePp, ScoreWithReplayavailablePp } from "./score.js"
import { Beatmap, Categories, Genres, Languages } from "./beatmap.js"
import { Match, MultiplayerModes, WinConditions } from "./match.js"
import { Mods, unsupported_mods } from "./mods.js"
import { Replay } from "./replay.js"
import { Gamemodes, getMods, getLength, getURL, adjustBeatmapStatsToMods } from "./misc.js"

export {Gamemodes, User, Score, ScoreWithBeatmapid, Mods, Replay}
export {Gamemodes, User, Score, ScoreWithBeatmapid, ScoreWithBeatmapidReplayavailablePp, ScoreWithReplayavailablePp, Mods, Replay}
export {Beatmap, Categories, Genres, Languages}
export {Match, MultiplayerModes, WinConditions}
export {getMods, getLength, getURL, adjustBeatmapStatsToMods}
Expand Down Expand Up @@ -103,7 +103,7 @@ export class API {
* @param endpoint Basically the endpoint, what comes in the URL after `api/`
* @param parameters The things to specify in the request, such as the beatmap_id when looking for a beatmap
* @param number_try How many attempts there's been to get the data
* @returns A Promise with either the API's response or `false` upon failing
* @returns A Promise with the API's response
*/
private async request(endpoint: string, parameters: string, number_try: number = 1): Promise<any> {
const max_tries = 5
Expand All @@ -123,7 +123,7 @@ export class API {
this.log(true, error.message)
err = `${error.name} (${error.errno})`
})

if (!response || !response.ok) {
if (response) {
err = response.statusText
Expand All @@ -146,7 +146,7 @@ export class API {

throw new APIError(err, this.server, endpoint, parameters)
}

this.log(false, response.statusText, response.status, {endpoint, parameters})
let json = await response.json()
if (Array.isArray(json) && !json.length) {
Expand All @@ -160,7 +160,7 @@ export class API {
throw new APIError("Match not available.", this.server, endpoint, parameters)
}
}
return json
return correctType(json)
}

/**
Expand All @@ -169,23 +169,32 @@ export class API {
* @returns A Promise with a `User` found with the search
*/
async getUser(gamemode: Gamemodes, user: {user_id?: number, username?: string} | User): Promise<User> {
let lookup = user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string`
let response = await this.request("get_user", `m=${gamemode}&${lookup}`) as User[]
return correctType(response[0])
const lookup = user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string`
const response = await this.request("get_user", `m=${gamemode}&${lookup}`) as User[]
return response[0]
}

/**
* @param limit The maximum number of Scores to get, **cannot exceed 100**
* @param gamemode The `User`'s `Gamemode`
* @param user An Object with either a `user_id` or a `username` (ignores `username` if `user_id` is specified)
* @param plays The `User`'s top pp plays/`Scores` or the `User`'s plays/`Scores` within the last 24 hours
* @returns A Promise with an array of `Scores` set by the `User` in a specific `Gamemode`
* @param user An Object with either a `user_id` or a `username` (`user_id` is preferred)
* @returns A Promise with an array of `Scores` set by the `User` within the last 24 hours in a specific `Gamemode`
*/
async getUserBestScores(limit: number, gamemode: Gamemodes, user: {user_id?: number, username?: string} | User):
Promise<ScoreWithBeatmapidReplayavailablePp[]> {
const lookup = user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string`
return await this.request("get_user_best", `${lookup}&m=${gamemode}&limit=${limit}`)
}

/**
* @param limit The maximum number of Scores to get, **cannot exceed 100**
* @param gamemode The `User`'s `Gamemode`
* @param user An Object with either a `user_id` or a `username` (`user_id` is preferred)
* @returns A Promise with an array of `Scores` set by the `User` within the last 24 hours in a specific `Gamemode`
*/
async getUserScores(limit: number, gamemode: Gamemodes, user: {user_id?: number, username?: string} | User, plays: "best" | "recent"):
Promise<ScoreWithBeatmapid[]> {
let lookup = user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string`
let response = await this.request(`get_user_${plays}`, `${lookup}&m=${gamemode}&limit=${limit}`) as Score[]
return correctType(response)
async getUserRecentScores(limit: number, gamemode: Gamemodes, user: {user_id?: number, username?: string} | User): Promise<ScoreWithBeatmapid[]> {
const lookup = user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string`
return await this.request("get_user_recent", `${lookup}&m=${gamemode}&limit=${limit}`)
}

/**
Expand All @@ -198,10 +207,10 @@ export class API {
async getBeatmap(beatmap: {beatmap_id: number} | Beatmap, mods: Mods = Mods.NONE, gamemode?: Gamemodes): Promise<Beatmap> {
if (getMods(mods).includes(Mods[Mods.NIGHTCORE]) && !getMods(mods).includes(Mods[Mods.DOUBLETIME])) {mods -= Mods.NIGHTCORE - Mods.DOUBLETIME}
unsupported_mods.forEach((mod) => getMods(mods!).includes(Mods[mod]) ? mods! -= mod : mods! -= 0)
let g = gamemode !== undefined ? `&mode=${gamemode}&a=1` : ""
const g = gamemode !== undefined ? `&mode=${gamemode}&a=1` : ""

let response = await this.request("get_beatmaps", `b=${beatmap.beatmap_id}&mods=${mods}${g}`) as Beatmap[]
return adjustBeatmapStatsToMods(correctType(response[0]), mods)
const response = await this.request("get_beatmaps", `b=${beatmap.beatmap_id}&mods=${mods}${g}`) as Beatmap[]
return adjustBeatmapStatsToMods(response[0], mods)
}

/**
Expand All @@ -222,10 +231,11 @@ export class API {
): Promise<Beatmap[]> {
if (getMods(mods).includes(Mods[Mods.NIGHTCORE]) && !getMods(mods).includes(Mods[Mods.DOUBLETIME])) {mods -= Mods.NIGHTCORE - Mods.DOUBLETIME}
unsupported_mods.forEach((mod) => getMods(mods!).includes(Mods[mod]) ? mods! -= mod : mods! -= 0)

const mode = gamemode.gamemode == "all" ? "" : `&m=${gamemode.gamemode}`
const convert = gamemode.allow_converts ? "a=1" : "a=0"
let lookup = `mods=${mods}`
let mode = gamemode.gamemode == "all" ? "" : `&m=${gamemode.gamemode}`
let convert = gamemode.allow_converts ? "a=1" : "a=0"


if (beatmap) {
if (beatmap.beatmapset_id !== undefined) {
lookup += `&s=${beatmap.beatmapset_id}`
Expand All @@ -249,9 +259,8 @@ export class API {
lookup += x.substring(0, x.indexOf("Z") - 4)
}

let response = await this.request("get_beatmaps", `limit=${limit}${mode}&${convert}${lookup}`) as Beatmap[]
let beatmaps: Beatmap[] = response.map((b: Beatmap) => adjustBeatmapStatsToMods(correctType(b), mods || Mods.NONE))
return beatmaps
const response = await this.request("get_beatmaps", `limit=${limit}${mode}&${convert}${lookup}`) as Beatmap[]
return response.map((b: Beatmap) => adjustBeatmapStatsToMods(correctType(b), mods || Mods.NONE))
}

/**
Expand All @@ -263,10 +272,9 @@ export class API {
* @returns A Promise with an array of `Scores` set on a beatmap
*/
async getBeatmapScores(limit: number, gamemode: Gamemodes, beatmap: {beatmap_id: number} | Beatmap,
user?: {user_id?: number, username?: string} | User, mods?: Mods): Promise<Score[]> {
let user_lookup = user ? user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string` : ""
let response = await this.request("get_scores", `b=${beatmap.beatmap_id}&m=${gamemode}${mods !== undefined ? "&mods="+mods : ""}${user_lookup}&limit=${limit}`)
return correctType(response) as Score[]
user?: {user_id?: number, username?: string} | User, mods?: Mods): Promise<ScoreWithReplayavailablePp[]> {
const user_lookup = user ? user.user_id !== undefined ? `u=${user.user_id}&type=id` : `u=${user.username}&type=string` : ""
return await this.request("get_scores", `b=${beatmap.beatmap_id}&m=${gamemode}${mods !== undefined ? "&mods="+mods : ""}${user_lookup}&limit=${limit}`)
}

/**
Expand All @@ -276,10 +284,9 @@ export class API {
* see https://docs.ripple.moe/docs/api/peppy
*/
async getMatch(id: number): Promise<Match> {
let response = await this.request("get_match", `mp=${id}`)
return correctType(response) as Match
return await this.request("get_match", `mp=${id}`)
}

/**
* Specify the gamemode the score was set in, then say if you know the id of the `Score` OR if you know the score's `User`, `Beatmap`, and `Mods`
* @param gamemode A number representing the `Gamemode` the `Score` was set in
Expand All @@ -290,9 +297,9 @@ export class API {
*/
async getReplay(gamemode: Gamemodes, replay: {score?: {score_id: number} | Score,
search?: {user: {user_id?: number, username?: string} | User, beatmap: {beatmap_id: number} | Beatmap, mods: Mods}}): Promise<Replay> {
const [score, search] = [replay.score, replay.search]
let lookup = ""
let [score, search] = [replay.score, replay.search]


if (score !== undefined) {
lookup = `s=${score.score_id}`
} else if (search !== undefined) {
Expand All @@ -305,7 +312,6 @@ export class API {
lookup += `&mods=${search.mods}`
}

let response = await this.request("get_replay", `${lookup}&m=${gamemode}`)
return correctType(response) as Replay
return await this.request("get_replay", `${lookup}&m=${gamemode}`)
}
}
50 changes: 25 additions & 25 deletions lib/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@
* https://osu.ppy.sh/wiki/en/Client/Interface/Multiplayer#team-mode-gameplay
*/
export enum MultiplayerModes {
"HEAD TO HEAD" = 0,
"TAG CO-OP" = 1,
"TEAM VS" = 2,
"TAG TEAM VS" = 3,
"HEAD TO HEAD" = 0,
"TAG CO-OP" = 1,
"TEAM VS" = 2,
"TAG TEAM VS" = 3,
}

/**
* https://osu.ppy.sh/wiki/en/Client/Interface/Multiplayer#win-condition
*/
export enum WinConditions {
SCORE = 0,
ACCURACY = 1,
COMBO = 2,
"SCORE V2" = 3,
SCORE = 0,
ACCURACY = 1,
COMBO = 2,
"SCORE V2" = 3,
}

export interface Match {
/**
* Has the info about the match that is not related to what's been played
*/
match: {
match_id: number,
name: string,
start_time: Date,
match_id: number
name: string
start_time: Date
/**
* If the match is not disbanded, null
*/
Expand All @@ -45,32 +45,32 @@ export interface Match {
*/
mods: number
scores: {
slot: number,
slot: number
/**
* 0 if no team, 1 if blue, 2 if red
*/
team: number,
user_id: number,
score: number,
maxcombo: number,
team: number
user_id: number
score: number
maxcombo: number
/**
* Is always 0, "is not used" according to documentation
*/
rank: number,
count50: number,
count100: number,
count300: number,
countmiss: number,
countgeki: number,
countkatu: number,
rank: number
count50: number
count100: number
count300: number
countmiss: number
countgeki: number
countkatu: number
/**
* Documentation says "If full combo", but should be "If SS/100% accuracy"
*/
perfect: boolean,
perfect: boolean
/**
* If the player is alive at the end of the map
*/
pass: boolean,
pass: boolean
/**
* Is null if no freemod
*/
Expand Down
Loading

0 comments on commit a3bfe5b

Please sign in to comment.