import { BsvMapDetail } from "shared/models/maps"; import { BsvPlaylistPage, MapFilter, PlaylistSearchParams, PlaylistSearchResponse, SearchParams, SearchResponse } from "shared/models/maps/beat-saver.model"; import { RequestService } from "../../request.service"; import { CustomError } from "shared/models/exceptions/custom-error.class"; export class BeatSaverApiService { private static instance: BeatSaverApiService; public static getInstance(): BeatSaverApiService { if (!BeatSaverApiService.instance) { BeatSaverApiService.instance = new BeatSaverApiService(); } return BeatSaverApiService.instance; } private readonly request: RequestService; private readonly bsaverApiUrl = "https://beatsaver.com/api"; private constructor() { this.request = RequestService.getInstance(); } private objectToStringRecord(obj: Record): Record { return Object.fromEntries(Object.entries(obj) .filter(([, value]) => value !== undefined && value !== null) .map(([key, value]) => [key, String(value)] as [string, string])); } private mapFilterToUrlParams(filter: MapFilter): URLSearchParams { if (!filter) { return new URLSearchParams(); } const enbledTagsString = filter.enabledTags ? Array.from(filter.enabledTags) : null; const excludedTagsString = filter.excludedTags ? Array.from(filter.excludedTags).map(tag => `!${tag}`) : null; let tags = [...(enbledTagsString ?? []), ...(excludedTagsString ?? [])].join("|"); tags = tags.length > 0 ? tags : null; const params: Record = this.objectToStringRecord({...filter, tags}); return new URLSearchParams(params); } private searchParamsToUrlParams(search: SearchParams): URLSearchParams { if (!search) { return new URLSearchParams(); } const searchParams = { ...(search.includeEmpty && { includeEmpty: String(search.includeEmpty) }), ...(search.sortOrder && { sortOrder: search.sortOrder }), ...(search.q && { q: search.q }), }; const filterUrlParms = this.mapFilterToUrlParams(search.filter); return new URLSearchParams({ ...searchParams, ...Object.fromEntries(filterUrlParms), }); } public async getMapsDetailsByHashs(hashs: T[]): Promise, BsvMapDetail>> { if (hashs.length > 50) { throw new CustomError("too musch map hashs", "TOO_MUCH_MAP_HASHS"); } const paramsHashs = hashs.join(","); const data = await this.request.getJSON, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`); if ((data as BsvMapDetail).id) { const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase(); const parsedData = { [key]: data as BsvMapDetail, } as Record, BsvMapDetail>; return parsedData } return data as Record, BsvMapDetail>; } public async getMapDetailsById(id: string): Promise { return this.request.getJSON(`${this.bsaverApiUrl}/maps/id/${id}`); } public searchMaps(search: SearchParams): Promise { const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`); url.search = this.searchParamsToUrlParams(search).toString(); return this.request.getJSON(url.toString()); } public searchPlaylists(search: PlaylistSearchParams): Promise { const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`); url.search = new URLSearchParams(this.objectToStringRecord(search)).toString(); return this.request.getJSON(url.toString()); } public getPlaylistDetailsById(id: string, page = 0): Promise { return this.request.getJSON(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`); } }