feat(beatmap,beatmapset): get beatmap & beatmapset information by id
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .auth import OAuthToken
|
||||
from .beatmap import (
|
||||
Beatmap as Beatmap,
|
||||
BeatmapResp as BeatmapResp,
|
||||
)
|
||||
from .beatmapset import (
|
||||
Beatmapset as Beatmapset,
|
||||
BeatmapsetResp as BeatmapsetResp,
|
||||
)
|
||||
from .legacy import LegacyOAuthToken, LegacyUserStatistics
|
||||
from .team import Team, TeamMember
|
||||
from .user import (
|
||||
@@ -23,7 +31,13 @@ from .user import (
|
||||
UserAvatar,
|
||||
)
|
||||
|
||||
BeatmapsetResp.model_rebuild()
|
||||
BeatmapResp.model_rebuild()
|
||||
__all__ = [
|
||||
"Beatmap",
|
||||
"BeatmapResp",
|
||||
"Beatmapset",
|
||||
"BeatmapsetResp",
|
||||
"DailyChallengeStats",
|
||||
"LazerUserAchievement",
|
||||
"LazerUserBadge",
|
||||
|
||||
94
app/database/beatmap.py
Normal file
94
app/database/beatmap.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# ruff: noqa: I002
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.beatmap import BeatmapRankStatus
|
||||
from app.models.score import MODE_TO_INT, GameMode
|
||||
|
||||
from .beatmapset import Beatmapset, BeatmapsetResp
|
||||
|
||||
from sqlalchemy import DECIMAL, Column, DateTime
|
||||
from sqlmodel import VARCHAR, Field, Relationship, SQLModel
|
||||
|
||||
|
||||
class BeatmapOwner(SQLModel):
|
||||
id: int
|
||||
username: str
|
||||
|
||||
|
||||
class BeatmapBase(SQLModel):
|
||||
# Beatmap
|
||||
url: str
|
||||
mode: GameMode
|
||||
beatmapset_id: int = Field(foreign_key="beatmapsets.id", index=True)
|
||||
difficulty_rating: float = Field(
|
||||
default=0.0, sa_column=Column(DECIMAL(precision=10, scale=6))
|
||||
)
|
||||
beatmap_status: BeatmapRankStatus
|
||||
total_length: int
|
||||
user_id: int
|
||||
version: str
|
||||
|
||||
# optional
|
||||
checksum: str = Field(sa_column=Column(VARCHAR(32), index=True))
|
||||
current_user_playcount: int = Field(default=0)
|
||||
max_combo: int = Field(default=0)
|
||||
# TODO: failtimes, owners
|
||||
|
||||
# BeatmapExtended
|
||||
ar: float = Field(default=0.0, sa_column=Column(DECIMAL(precision=10, scale=2)))
|
||||
cs: float = Field(default=0.0, sa_column=Column(DECIMAL(precision=10, scale=2)))
|
||||
drain: float = Field(
|
||||
default=0.0,
|
||||
sa_column=Column(DECIMAL(precision=10, scale=2)),
|
||||
) # hp
|
||||
accuracy: float = Field(
|
||||
default=0.0,
|
||||
sa_column=Column(DECIMAL(precision=10, scale=2)),
|
||||
) # od
|
||||
bpm: float = Field(default=0.0, sa_column=Column(DECIMAL(precision=10, scale=2)))
|
||||
count_circles: int = Field(default=0)
|
||||
count_sliders: int = Field(default=0)
|
||||
count_spinners: int = Field(default=0)
|
||||
deleted_at: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||||
hit_length: int = Field(default=0)
|
||||
last_updated: datetime = Field(sa_column=Column(DateTime))
|
||||
passcount: int = Field(default=0)
|
||||
playcount: int = Field(default=0)
|
||||
|
||||
|
||||
class Beatmap(BeatmapBase, table=True):
|
||||
__tablename__ = "beatmaps" # pyright: ignore[reportAssignmentType]
|
||||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||
beatmapset_id: int = Field(foreign_key="beatmapsets.id", index=True)
|
||||
# optional
|
||||
beatmapset: Beatmapset = Relationship(back_populates="beatmaps")
|
||||
|
||||
|
||||
class BeatmapResp(BeatmapBase):
|
||||
id: int
|
||||
beatmapset_id: int
|
||||
beatmapset: BeatmapsetResp | None = None
|
||||
convert: bool = False
|
||||
is_scoreable: bool
|
||||
status: str
|
||||
mode_int: int
|
||||
ranked: int
|
||||
url: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_db(
|
||||
cls,
|
||||
beatmap: Beatmap,
|
||||
query_mode: GameMode | None = None,
|
||||
from_set: bool = False,
|
||||
) -> "BeatmapResp":
|
||||
beatmap_ = beatmap.model_dump()
|
||||
if query_mode is not None and beatmap.mode != query_mode:
|
||||
beatmap_["convert"] = True
|
||||
beatmap_["is_scoreable"] = beatmap.beatmap_status > BeatmapRankStatus.PENDING
|
||||
beatmap_["status"] = beatmap.beatmap_status.name.lower()
|
||||
beatmap_["ranked"] = beatmap.beatmap_status.value
|
||||
beatmap_["mode_int"] = MODE_TO_INT[beatmap.mode]
|
||||
if not from_set:
|
||||
beatmap_["beatmapset"] = BeatmapsetResp.from_db(beatmap.beatmapset)
|
||||
return cls.model_validate(beatmap_)
|
||||
205
app/database/beatmapset.py
Normal file
205
app/database/beatmapset.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# ruff: noqa: I002
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from app.models.beatmap import BeatmapRankStatus, Genre, Language
|
||||
from app.models.score import GameMode
|
||||
|
||||
from pydantic import BaseModel, model_serializer
|
||||
from sqlalchemy import DECIMAL, JSON, Column, DateTime, Text
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .beatmap import Beatmap, BeatmapResp
|
||||
|
||||
|
||||
class BeatmapCovers(SQLModel):
|
||||
cover: str
|
||||
card: str
|
||||
list: str
|
||||
slimcover: str
|
||||
cover_2_x: str | None = Field(default=None, alias="cover@2x")
|
||||
card_2_x: str | None = Field(default=None, alias="card@2x")
|
||||
list_2_x: str | None = Field(default=None, alias="list@2x")
|
||||
slimcover_2_x: str | None = Field(default=None, alias="slimcover@2x")
|
||||
|
||||
@model_serializer
|
||||
def _(self) -> dict[str, str | None]:
|
||||
self = cast(dict[str, str | None] | BeatmapCovers, self)
|
||||
if isinstance(self, dict):
|
||||
return {
|
||||
"cover": self["cover"],
|
||||
"card": self["card"],
|
||||
"list": self["list"],
|
||||
"slimcover": self["slimcover"],
|
||||
"cover@2x": self.get("cover@2x"),
|
||||
"card@2x": self.get("card@2x"),
|
||||
"list@2x": self.get("list@2x"),
|
||||
"slimcover@2x": self.get("slimcover@2x"),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"cover": self.cover,
|
||||
"card": self.card,
|
||||
"list": self.list,
|
||||
"slimcover": self.slimcover,
|
||||
"cover@2x": self.cover_2_x,
|
||||
"card@2x": self.card_2_x,
|
||||
"list@2x": self.list_2_x,
|
||||
"slimcover@2x": self.slimcover_2_x,
|
||||
}
|
||||
|
||||
|
||||
class BeatmapHype(BaseModel):
|
||||
current: int
|
||||
required: int
|
||||
|
||||
|
||||
class BeatmapAvailability(BaseModel):
|
||||
more_information: str | None = Field(default=None)
|
||||
download_disabled: bool | None = Field(default=None)
|
||||
|
||||
|
||||
class BeatmapNominations(SQLModel):
|
||||
current: int | None = Field(default=None)
|
||||
required: int | None = Field(default=None)
|
||||
|
||||
|
||||
class BeatmapNomination(SQLModel):
|
||||
beatmapset_id: int
|
||||
reset: bool
|
||||
user_id: int
|
||||
rulesets: list[GameMode] | None = None
|
||||
|
||||
|
||||
class BeatmapDescription(SQLModel):
|
||||
bbcode: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class BeatmapTranslationText(BaseModel):
|
||||
name: str
|
||||
id: int | None = None
|
||||
|
||||
|
||||
class BeatmapsetBase(SQLModel):
|
||||
# Beatmapset
|
||||
artist: str = Field(index=True)
|
||||
artist_unicode: str = Field(index=True)
|
||||
covers: BeatmapCovers = Field(sa_column=Column(JSON))
|
||||
creator: str
|
||||
favourite_count: int
|
||||
nsfw: bool = Field(default=False)
|
||||
play_count: int
|
||||
preview_url: str
|
||||
source: str = Field(default="")
|
||||
|
||||
spotlight: bool = Field(default=False)
|
||||
title: str
|
||||
title_unicode: str
|
||||
user_id: int
|
||||
video: bool
|
||||
|
||||
# optional
|
||||
# converts: list[Beatmap] = Relationship(back_populates="beatmapset")
|
||||
current_nominations: list[BeatmapNomination] | None = Field(
|
||||
None, sa_column=Column(JSON)
|
||||
)
|
||||
description: BeatmapDescription | None = Field(default=None, sa_column=Column(JSON))
|
||||
# TODO: discussions: list[BeatmapsetDiscussion] = None
|
||||
# TODO: current_user_attributes: Optional[CurrentUserAttributes] = None
|
||||
# TODO: events: Optional[list[BeatmapsetEvent]] = None
|
||||
|
||||
pack_tags: list[str] = Field(default=[], sa_column=Column(JSON))
|
||||
ratings: list[int] = Field(default=None, sa_column=Column(JSON))
|
||||
# TODO: recent_favourites: Optional[list[User]] = None
|
||||
# TODO: related_users: Optional[list[User]] = None
|
||||
# TODO: user: Optional[User] = Field(default=None)
|
||||
track_id: int | None = Field(default=None) # feature artist?
|
||||
# TODO: has_favourited
|
||||
|
||||
# BeatmapsetExtended
|
||||
bpm: float = Field(default=0.0, sa_column=Column(DECIMAL(10, 2)))
|
||||
can_be_hyped: bool = Field(default=False)
|
||||
discussion_locked: bool = Field(default=False)
|
||||
last_updated: datetime = Field(sa_column=Column(DateTime))
|
||||
ranked_date: datetime | None = Field(default=None, sa_column=Column(DateTime))
|
||||
storyboard: bool = Field(default=False)
|
||||
submitted_date: datetime = Field(sa_column=Column(DateTime))
|
||||
tags: str = Field(default="", sa_column=Column(Text))
|
||||
|
||||
|
||||
class Beatmapset(BeatmapsetBase, table=True):
|
||||
__tablename__ = "beatmapsets" # pyright: ignore[reportAssignmentType]
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||
# Beatmapset
|
||||
beatmap_status: BeatmapRankStatus = Field(
|
||||
default=BeatmapRankStatus.GRAVEYARD,
|
||||
)
|
||||
|
||||
# optional
|
||||
beatmaps: list["Beatmap"] = Relationship(back_populates="beatmapset")
|
||||
beatmap_genre: Genre = Field(default=Genre.UNSPECIFIED)
|
||||
beatmap_language: Language = Field(default=Language.UNSPECIFIED)
|
||||
nominations_required: int = Field(default=0)
|
||||
nominations_current: int = Field(default=0)
|
||||
|
||||
# BeatmapExtended
|
||||
hype_current: int = Field(default=0)
|
||||
hype_required: int = Field(default=0)
|
||||
availability_info: str | None = Field(default=None)
|
||||
download_disabled: bool = Field(default=False)
|
||||
|
||||
|
||||
class BeatmapsetResp(BeatmapsetBase):
|
||||
id: int
|
||||
beatmaps: list["BeatmapResp"]
|
||||
discussion_enabled: bool = True
|
||||
status: str
|
||||
ranked: int
|
||||
legacy_thread_url: str = ""
|
||||
is_scoreable: bool
|
||||
hype: BeatmapHype
|
||||
availability: BeatmapAvailability
|
||||
genre: BeatmapTranslationText
|
||||
language: BeatmapTranslationText
|
||||
nominations: BeatmapNominations
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, beatmapset: Beatmapset) -> "BeatmapsetResp":
|
||||
from .beatmap import BeatmapResp
|
||||
|
||||
beatmaps = [
|
||||
BeatmapResp.from_db(beatmap, from_set=True)
|
||||
for beatmap in beatmapset.beatmaps
|
||||
]
|
||||
return cls.model_validate(
|
||||
{
|
||||
"beatmaps": beatmaps,
|
||||
"hype": BeatmapHype(
|
||||
current=beatmapset.hype_current, required=beatmapset.hype_required
|
||||
),
|
||||
"availability": BeatmapAvailability(
|
||||
more_information=beatmapset.availability_info,
|
||||
download_disabled=beatmapset.download_disabled,
|
||||
),
|
||||
"genre": BeatmapTranslationText(
|
||||
name=beatmapset.beatmap_genre.name,
|
||||
id=beatmapset.beatmap_genre.value,
|
||||
),
|
||||
"language": BeatmapTranslationText(
|
||||
name=beatmapset.beatmap_language.name,
|
||||
id=beatmapset.beatmap_language.value,
|
||||
),
|
||||
"nominations": BeatmapNominations(
|
||||
required=beatmapset.nominations_required,
|
||||
current=beatmapset.nominations_current,
|
||||
),
|
||||
"status": beatmapset.beatmap_status.name.lower(),
|
||||
"ranked": beatmapset.beatmap_status.value,
|
||||
"is_scoreable": beatmapset.beatmap_status > BeatmapRankStatus.PENDING,
|
||||
**beatmapset.model_dump(),
|
||||
}
|
||||
)
|
||||
@@ -8,7 +8,7 @@ from .team import TeamMember
|
||||
|
||||
from sqlalchemy import DECIMAL, JSON, Column, Date, DateTime, Text
|
||||
from sqlalchemy.dialects.mysql import VARCHAR
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
@@ -263,10 +263,10 @@ class LazerUserStatistics(SQLModel, table=True):
|
||||
# PP 和分数
|
||||
pp: float = Field(default=0.00, sa_column=Column(DECIMAL(10, 2)))
|
||||
pp_exp: float = Field(default=0.00, sa_column=Column(DECIMAL(10, 2)))
|
||||
ranked_score: int = Field(default=0)
|
||||
ranked_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||||
hit_accuracy: float = Field(default=0.00, sa_column=Column(DECIMAL(5, 2)))
|
||||
total_score: int = Field(default=0)
|
||||
total_hits: int = Field(default=0)
|
||||
total_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||||
total_hits: int = Field(default=0, sa_column=Column(BigInteger))
|
||||
maximum_combo: int = Field(default=0)
|
||||
|
||||
# 游戏统计
|
||||
@@ -406,6 +406,7 @@ class UserAchievement:
|
||||
achieved_at: datetime
|
||||
achievement_id: int
|
||||
|
||||
|
||||
class DailyChallengeStats(SQLModel, table=True):
|
||||
__tablename__ = "daily_challenge_stats" # pyright: ignore[reportAssignmentType]
|
||||
|
||||
@@ -441,7 +442,6 @@ class RankHistory(SQLModel, table=True):
|
||||
user: "User" = Relationship(back_populates="rank_history")
|
||||
|
||||
|
||||
|
||||
class UserAvatar(SQLModel, table=True):
|
||||
__tablename__ = "user_avatars" # pyright: ignore[reportAssignmentType]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user