From aea6c77fd052a4a74795442893da06f9a9c6c79b Mon Sep 17 00:00:00 2001 From: cmyui Date: Fri, 21 Jun 2024 22:36:48 -0400 Subject: [PATCH] Cheesegull Beatmapset API --- app/adapters/osu_api_v2.py | 117 +++++++++++++++++++++++++++++++++++- app/api/v1/cheesegull.py | 119 ++++++++++++++++++++++++++----------- 2 files changed, 200 insertions(+), 36 deletions(-) diff --git a/app/adapters/osu_api_v2.py b/app/adapters/osu_api_v2.py index 854c6f8..1970e84 100644 --- a/app/adapters/osu_api_v2.py +++ b/app/adapters/osu_api_v2.py @@ -1,12 +1,15 @@ from datetime import datetime from enum import StrEnum +from typing import Any import httpx from pydantic import BaseModel +from pydantic import Field from app import oauth from app import settings + http_client = httpx.AsyncClient( base_url="https://osu.ppy.sh/api/v2/", auth=oauth.AsyncOAuth( @@ -48,7 +51,8 @@ class Beatmap(BaseModel): checksum: str | None failtimes: Failtimes - max_combo: int + max_combo: int | None = None + bpm: int class BeatmapExtended(Beatmap): @@ -77,3 +81,114 @@ async def get_beatmap(beatmap_id: int) -> BeatmapExtended: response = await http_client.get(f"beatmaps/{beatmap_id}") response.raise_for_status() return BeatmapExtended(**response.json()) + + +class Covers(BaseModel): + # cover string + # cover@2x string + # card string + # card@2x string + # list string + # list@2x string + # slimcover string + # slimcover@2x string + cover: str + cover2x: str = Field(alias="cover@2x") + card: str + card2x: str = Field(alias="card@2x") + list: str + list2x: str = Field(alias="list@2x") + slimcover: str + slimcover2x: str = Field(alias="slimcover@2x") + + +class RequiredMeta(BaseModel): + main_ruleset: int + non_main_ruleset: int + + +class NominationsSummary(BaseModel): + current: int + eligible_main_rulesets: list[Ruleset] + required_meta: RequiredMeta + + +class Availability(BaseModel): + download_disabled: bool + more_information: str | None + + +class Genre(BaseModel): + id: int + name: str + + +class Description(BaseModel): + description: str # (html string) + + +class Language(BaseModel): + id: int + name: str + + +class Beatmapset(BaseModel): + artist: str + artist_unicode: str | None + covers: Covers + creator: str + favourite_count: int + hype: Any | None # TODO + id: int + nsfw: bool + offset: int + play_count: int + preview_url: str + source: str + spotlight: bool + status: str # TODO enum + title: str + title_unicode: str | None + user_id: int + video: bool + + bpm: int | None + can_be_hyped: bool + deleted_at: datetime | None + discussion_enabled: bool + discussion_locked: bool + is_scoreable: bool + last_updated: datetime + legacy_thread_url: str + nominations_summary: NominationsSummary + ranked: int # TODO: enum + ranked_date: datetime | None + storyboard: bool + submitted_date: datetime + tags: str + + availability: Availability + + beatmaps: list[BeatmapExtended] | None + converts: list[BeatmapExtended] + current_nominations: list[str] | None + current_user_attributes: Any | None = None # TODO + description: Description + discussions: Any = None # TODO + events: Any | None = None # TODO + genre: Genre | None = None + has_favourited: Any = None # TODO + language: Language | None = None # TODO + pack_tags: list[str] | None + ratings: Any # TODO + recent_favourites: Any # TODO + related_users: Any # TODO + user: Any # TODO + track_id: int | None + + +async def get_beatmapset(beatmapset_id: int) -> Beatmapset: + response = await http_client.get(f"beatmapsets/{beatmapset_id}") + response.raise_for_status() + xd = response.json() + return Beatmapset(**xd) diff --git a/app/api/v1/cheesegull.py b/app/api/v1/cheesegull.py index 97e943b..9395b43 100644 --- a/app/api/v1/cheesegull.py +++ b/app/api/v1/cheesegull.py @@ -1,3 +1,5 @@ +from datetime import datetime + from fastapi import APIRouter from pydantic import BaseModel @@ -7,22 +9,6 @@ class CheesegullBeatmap(BaseModel): - # BeatmapID: 315, - # ParentSetID: 141, - # DiffName: "Insane", - # FileMD5: "1cf5b2c2edfafd055536d2cefcb89c0e", - # Mode: 0, - # BPM: 168, - # AR: 7, - # OD: 7, - # CS: 6, - # HP: 2, - # TotalLength: 14, - # HitLength: 14, - # Playcount: 1767740, - # Passcount: 1074297, - # MaxCombo: 114, - # DifficultyRating: 5.23 BeatmapID: int ParentSetID: int DiffName: str @@ -40,31 +26,94 @@ class CheesegullBeatmap(BaseModel): MaxCombo: int DifficultyRating: float + @classmethod + def from_osu_api_beatmap( + cls, + beatmap: osu_api_v2.BeatmapExtended, + ) -> "CheesegullBeatmap": + return cls( + BeatmapID=beatmap.id, + ParentSetID=beatmap.beatmapset_id, + DiffName=beatmap.version, + FileMD5=beatmap.checksum or "", + Mode=beatmap.mode_int, + BPM=beatmap.bpm or 0, + AR=beatmap.ar, + OD=beatmap.accuracy, + CS=beatmap.cs, + HP=beatmap.drain, + TotalLength=beatmap.total_length, + HitLength=beatmap.total_length, + Playcount=beatmap.playcount, + Passcount=beatmap.passcount, + MaxCombo=beatmap.max_combo or 0, + DifficultyRating=beatmap.difficulty_rating, + ) + + +class CheesegullBeatmapset(BaseModel): + SetID: int + ChildrenBeatmaps: list[CheesegullBeatmap] + RankedStatus: int + ApprovedDate: datetime + LastUpdate: datetime + LastChecked: datetime + Artist: str + Title: str + Creator: str + Source: str + Tags: str + HasVideo: bool + Genre: int | None + Language: int | None + Favourites: int + + @classmethod + def from_osu_api_beatmapset( + cls, + osu_api_beatmapset: osu_api_v2.Beatmapset, + ) -> "CheesegullBeatmapset": + children_beatmaps: list[CheesegullBeatmap] = [] + for osu_api_beatmap in osu_api_beatmapset.beatmaps or []: + if not isinstance(osu_api_beatmap, osu_api_v2.BeatmapExtended): + raise ValueError("beatmapset.beatmaps is not a list of BeatmapExtended") + cheesegull_beatmap = CheesegullBeatmap.from_osu_api_beatmap(osu_api_beatmap) + children_beatmaps.append(cheesegull_beatmap) + + return cls( + SetID=osu_api_beatmapset.id, + ChildrenBeatmaps=children_beatmaps, + RankedStatus=osu_api_beatmapset.ranked, + ApprovedDate=osu_api_beatmapset.ranked_date or datetime.min, + LastUpdate=( + osu_api_beatmapset.ranked_date or osu_api_beatmapset.last_updated + ), + LastChecked=datetime.now(), # TODO: Implement this + Artist=osu_api_beatmapset.artist, + Title=osu_api_beatmapset.title, + Creator=osu_api_beatmapset.creator, + Source=osu_api_beatmapset.source, + Tags=osu_api_beatmapset.tags, + HasVideo=osu_api_beatmapset.video, + Genre=osu_api_beatmapset.genre.id if osu_api_beatmapset.genre else None, + Language=( + osu_api_beatmapset.language.id if osu_api_beatmapset.language else None + ), + Favourites=osu_api_beatmapset.favourite_count, + ) + @router.get("/api/v1/cheesegull/b/{beatmap_id}") async def cheesegull_beatmap(beatmap_id: int): osu_api_beatmap = await osu_api_v2.get_beatmap(beatmap_id) - cheesegull_beatmap = CheesegullBeatmap( - BeatmapID=osu_api_beatmap.id, - ParentSetID=osu_api_beatmap.beatmapset_id, - DiffName=osu_api_beatmap.version, - FileMD5=osu_api_beatmap.checksum or "", - Mode=osu_api_beatmap.mode_int, - BPM=osu_api_beatmap.bpm or 0, - AR=osu_api_beatmap.ar, - OD=osu_api_beatmap.accuracy, - CS=osu_api_beatmap.cs, - HP=osu_api_beatmap.drain, - TotalLength=osu_api_beatmap.total_length, - HitLength=osu_api_beatmap.total_length, - Playcount=osu_api_beatmap.playcount, - Passcount=osu_api_beatmap.passcount, - MaxCombo=osu_api_beatmap.max_combo, - DifficultyRating=osu_api_beatmap.difficulty_rating, - ) + cheesegull_beatmap = CheesegullBeatmap.from_osu_api_beatmap(osu_api_beatmap) return cheesegull_beatmap.model_dump() @router.get("/api/v1/cheesegull/s/{beatmapset_id}") async def cheesegull_beatmapset(beatmapset_id: int): - return {"beatmapset_id": beatmapset_id} + osu_api_beatmapset = await osu_api_v2.get_beatmapset(beatmapset_id) + cheesegull_beatmapset = CheesegullBeatmapset.from_osu_api_beatmapset( + osu_api_beatmapset, + ) + return cheesegull_beatmapset.model_dump()