From 6f899f0319d3b9e2ab1ed1453252580f2c1f7311 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:51:51 +0200 Subject: [PATCH 1/3] feat: Add bancho support for GetBeatmapByHash --- .../core/domains/osu.ppy.sh/bancho.client.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/core/domains/osu.ppy.sh/bancho.client.ts b/server/src/core/domains/osu.ppy.sh/bancho.client.ts index 35b4917..1f4564c 100644 --- a/server/src/core/domains/osu.ppy.sh/bancho.client.ts +++ b/server/src/core/domains/osu.ppy.sh/bancho.client.ts @@ -24,6 +24,7 @@ export class BanchoClient extends BaseClient { baseUrl: "https://osu.ppy.sh", abilities: [ ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapByHash, ClientAbilities.GetBeatmapSetById, ClientAbilities.GetBeatmaps, ClientAbilities.DownloadOsuBeatmap, @@ -31,10 +32,15 @@ export class BanchoClient extends BaseClient { ], }, { + headers: { + remaining: "x-ratelimit-remaining", + limit: "x-ratelimit-limit", + }, rateLimits: [ { abilities: [ ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapByHash, ClientAbilities.GetBeatmapSetById, ClientAbilities.GetBeatmaps, ClientAbilities.DownloadOsuBeatmap, @@ -67,6 +73,9 @@ export class BanchoClient extends BaseClient { if (ctx.beatmapId) { return await this.getBeatmapById(ctx.beatmapId); } + else if (ctx.beatmapHash) { + return await this.getBeatmapByHash(ctx.beatmapHash); + } throw new Error("Invalid arguments"); } @@ -199,6 +208,27 @@ export class BanchoClient extends BaseClient { }; } + private async getBeatmapByHash( + beatmapHash: string, + ): Promise> { + const result = await this.api.get(`api/v2/beatmaps/lookup?checksum=${beatmapHash}`, { + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, + }, + }, + }); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; + } + + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } + private get osuApiKey() { return this.banchoService.getBanchoClientToken(); } From 92b1ae42abefb48be4551f7466efbd42d255aad8 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:02:52 +0200 Subject: [PATCH 2/3] feat: Add bancho search support --- server/src/controllers/api/index.ts | 8 ++- .../core/domains/osu.ppy.sh/bancho.client.ts | 66 ++++++++++++++++++- server/src/types/general/api.ts | 1 + 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 server/src/types/general/api.ts diff --git a/server/src/controllers/api/index.ts b/server/src/controllers/api/index.ts index 3dce9da..3e642ee 100644 --- a/server/src/controllers/api/index.ts +++ b/server/src/controllers/api/index.ts @@ -2,6 +2,7 @@ import { t } from "elysia"; import type { App } from "../../app"; import { BeatmapsManagerPlugin } from "../../plugins/beatmapManager"; +import { BEATMAPS_SEARCH_MAX_RESULTS_LIMIT } from "../../types/general/api"; export default (app: App) => { app.use(BeatmapsManagerPlugin) @@ -121,7 +122,12 @@ export default (app: App) => { async ({ BeatmapsManagerInstance, query, set }) => { // TODO: Add another search endpoint which would parse cursors instead of pages, to create compatibility with bancho api; + if (query.limit && (query.limit > BEATMAPS_SEARCH_MAX_RESULTS_LIMIT || query.limit < 1)) { + throw new Error(`Limit is too high. Maximum limit is ${BEATMAPS_SEARCH_MAX_RESULTS_LIMIT}`); + } + const data = await BeatmapsManagerInstance.searchBeatmapsets({ + limit: query.limit ?? 50, ...query, }); @@ -135,7 +141,7 @@ export default (app: App) => { { query: t.Object({ query: t.Optional(t.String()), - limit: t.Optional(t.Numeric()), + limit: t.Optional(t.Numeric({ min: 1, max: BEATMAPS_SEARCH_MAX_RESULTS_LIMIT })), offset: t.Optional(t.Numeric()), status: t.Optional(t.Array(t.Numeric())), mode: t.Optional(t.Numeric()), diff --git a/server/src/core/domains/osu.ppy.sh/bancho.client.ts b/server/src/core/domains/osu.ppy.sh/bancho.client.ts index 1f4564c..fc392b0 100644 --- a/server/src/core/domains/osu.ppy.sh/bancho.client.ts +++ b/server/src/core/domains/osu.ppy.sh/bancho.client.ts @@ -1,3 +1,6 @@ +import qs from "qs"; + +import { BEATMAPS_SEARCH_MAX_RESULTS_LIMIT } from "../../../types/general/api"; import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; import logger from "../../../utils/logger"; import { BaseClient } from "../../abstracts/client/base-client.abstract"; @@ -8,12 +11,16 @@ import type { GetBeatmapsetsByBeatmapIdsOptions, GetBeatmapsOptions, ResultWithStatus, + SearchBeatmapsetsOptions, } from "../../abstracts/client/base-client.types"; import { ClientAbilities, } from "../../abstracts/client/base-client.types"; import { BanchoService } from "./bancho-client.service"; -import type { BanchoBeatmap, BanchoBeatmapset } from "./bancho-client.types"; +import type { BanchoBeatmap, BanchoBeatmapset, BanchoBeatmapsetSearchResult } from "./bancho-client.types"; + +const BANCHO_SEARCH_PAGE_SIZE = 50; +const BANCHO_SEARCH_PAGES_LIMIT = 200; export class BanchoClient extends BaseClient { private readonly banchoService = new BanchoService(this.baseApi); @@ -29,6 +36,7 @@ export class BanchoClient extends BaseClient { ClientAbilities.GetBeatmaps, ClientAbilities.DownloadOsuBeatmap, ClientAbilities.GetBeatmapsetsByBeatmapIds, + ClientAbilities.SearchBeatmapsets, ], }, { @@ -45,6 +53,7 @@ export class BanchoClient extends BaseClient { ClientAbilities.GetBeatmaps, ClientAbilities.DownloadOsuBeatmap, ClientAbilities.GetBeatmapsetsByBeatmapIds, + ClientAbilities.SearchBeatmapsets, ], routes: ["/"], limit: 1200, @@ -160,6 +169,61 @@ export class BanchoClient extends BaseClient { return { result: result.data, status: result.status }; } + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + const page = Math.floor((ctx.offset ?? 0) / BANCHO_SEARCH_PAGE_SIZE) + 1; + + if (page > BANCHO_SEARCH_PAGES_LIMIT + || (ctx.limit && ctx.limit > BANCHO_SEARCH_PAGE_SIZE && page === BANCHO_SEARCH_PAGES_LIMIT)) { + return { result: null, status: 500 }; // Bancho API has a limit of 200 pages + } + + const result = await this.api.get(`api/v2/beatmapsets/search`, { + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, + }, + params: { + query: ctx.query, + page, + status: ctx.status, + mode: ctx.mode, + }, + paramsSerializer: params => + qs.stringify(params, { indices: false }), + }, + }); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; + } + + let { beatmapsets } = result.data; + let additionalBeatmapsets: BanchoBeatmapset[] = []; + + if (ctx.limit && ctx.limit <= BEATMAPS_SEARCH_MAX_RESULTS_LIMIT) { + if (ctx.limit < BANCHO_SEARCH_PAGE_SIZE) { + beatmapsets = beatmapsets.slice(0, ctx.limit); + } + + if (ctx.limit > BANCHO_SEARCH_PAGE_SIZE && page < BANCHO_SEARCH_PAGES_LIMIT) { + additionalBeatmapsets = await this.searchBeatmapsets({ + ...ctx, + limit: ctx.limit - beatmapsets.length, + offset: (ctx.offset ?? 0) + beatmapsets.length, + }).then(result => result.result ?? []); + } + } + + return { + result: [...additionalBeatmapsets, ...(beatmapsets.map((b: BanchoBeatmapset) => + this.convertService.convertBeatmapset(b), + ))], + status: result.status, + }; + } + private async getBeatmapSetById( beatmapSetId: number, ): Promise> { diff --git a/server/src/types/general/api.ts b/server/src/types/general/api.ts new file mode 100644 index 0000000..2cf3361 --- /dev/null +++ b/server/src/types/general/api.ts @@ -0,0 +1 @@ +export const BEATMAPS_SEARCH_MAX_RESULTS_LIMIT = 100; From 88fd94520471950820e009418eeb6028b7ec6661 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:13:16 +0200 Subject: [PATCH 3/3] feat: Prioritise one single mirror for a chain of search requests --- .../core/managers/mirrors/mirrors.manager.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/server/src/core/managers/mirrors/mirrors.manager.ts b/server/src/core/managers/mirrors/mirrors.manager.ts index 2ab18be..63415d9 100644 --- a/server/src/core/managers/mirrors/mirrors.manager.ts +++ b/server/src/core/managers/mirrors/mirrors.manager.ts @@ -328,11 +328,32 @@ export class MirrorsManager { ) .filter(client => !ignore || !ignore.includes(client)); - const client = this.getClientByWeight(criteria, clients); + const isSearchCriteria = criteria === ClientAbilities.SearchBeatmapsets; + + const client = isSearchCriteria + ? this.getClientForSearchEndpoint(clients) + : this.getClientByWeight(criteria, clients); return client; } + // We want to prioritise a single mirror for all search endpoints while it's available to have consistent results from page to page. + private getClientForSearchEndpoint(clients: MirrorClient[]): MirrorClient | null { + const minoClient = clients.find(client => client.client.clientConfig.baseUrl === "https://us.catboy.best" + || client.client.clientConfig.baseUrl === "https://catboy.best"); // TODO: I don't like how we address them by url, should be addressed later. + + if (minoClient) { + return minoClient; + } + + const banchoClient = clients.find(client => client.client.clientConfig.baseUrl === "https://osu.ppy.sh"); + if (banchoClient) { + return banchoClient; + } + + return this.getClientByWeight(ClientAbilities.SearchBeatmapsets, clients); + } + private getClientByWeight( criteria: ClientAbilities, clients: MirrorClient[],