feat(beatmap,beatmapset): get beatmap & beatmapset information by id
This commit is contained in:
@@ -159,6 +159,13 @@ def store_token(
|
|||||||
for token in old_tokens:
|
for token in old_tokens:
|
||||||
db.delete(token)
|
db.delete(token)
|
||||||
|
|
||||||
|
# 检查是否有重复的 access_token
|
||||||
|
duplicate_token = db.exec(
|
||||||
|
select(OAuthToken).where(OAuthToken.access_token == access_token)
|
||||||
|
).first()
|
||||||
|
if duplicate_token:
|
||||||
|
db.delete(duplicate_token)
|
||||||
|
|
||||||
# 创建新令牌记录
|
# 创建新令牌记录
|
||||||
token_record = OAuthToken(
|
token_record = OAuthToken(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .auth import OAuthToken
|
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 .legacy import LegacyOAuthToken, LegacyUserStatistics
|
||||||
from .team import Team, TeamMember
|
from .team import Team, TeamMember
|
||||||
from .user import (
|
from .user import (
|
||||||
@@ -23,7 +31,13 @@ from .user import (
|
|||||||
UserAvatar,
|
UserAvatar,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BeatmapsetResp.model_rebuild()
|
||||||
|
BeatmapResp.model_rebuild()
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Beatmap",
|
||||||
|
"BeatmapResp",
|
||||||
|
"Beatmapset",
|
||||||
|
"BeatmapsetResp",
|
||||||
"DailyChallengeStats",
|
"DailyChallengeStats",
|
||||||
"LazerUserAchievement",
|
"LazerUserAchievement",
|
||||||
"LazerUserBadge",
|
"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 import DECIMAL, JSON, Column, Date, DateTime, Text
|
||||||
from sqlalchemy.dialects.mysql import VARCHAR
|
from sqlalchemy.dialects.mysql import VARCHAR
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
class User(SQLModel, table=True):
|
class User(SQLModel, table=True):
|
||||||
@@ -263,10 +263,10 @@ class LazerUserStatistics(SQLModel, table=True):
|
|||||||
# PP 和分数
|
# PP 和分数
|
||||||
pp: float = Field(default=0.00, sa_column=Column(DECIMAL(10, 2)))
|
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)))
|
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)))
|
hit_accuracy: float = Field(default=0.00, sa_column=Column(DECIMAL(5, 2)))
|
||||||
total_score: int = Field(default=0)
|
total_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||||||
total_hits: int = Field(default=0)
|
total_hits: int = Field(default=0, sa_column=Column(BigInteger))
|
||||||
maximum_combo: int = Field(default=0)
|
maximum_combo: int = Field(default=0)
|
||||||
|
|
||||||
# 游戏统计
|
# 游戏统计
|
||||||
@@ -406,6 +406,7 @@ class UserAchievement:
|
|||||||
achieved_at: datetime
|
achieved_at: datetime
|
||||||
achievement_id: int
|
achievement_id: int
|
||||||
|
|
||||||
|
|
||||||
class DailyChallengeStats(SQLModel, table=True):
|
class DailyChallengeStats(SQLModel, table=True):
|
||||||
__tablename__ = "daily_challenge_stats" # pyright: ignore[reportAssignmentType]
|
__tablename__ = "daily_challenge_stats" # pyright: ignore[reportAssignmentType]
|
||||||
|
|
||||||
@@ -441,7 +442,6 @@ class RankHistory(SQLModel, table=True):
|
|||||||
user: "User" = Relationship(back_populates="rank_history")
|
user: "User" = Relationship(back_populates="rank_history")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserAvatar(SQLModel, table=True):
|
class UserAvatar(SQLModel, table=True):
|
||||||
__tablename__ = "user_avatars" # pyright: ignore[reportAssignmentType]
|
__tablename__ = "user_avatars" # pyright: ignore[reportAssignmentType]
|
||||||
|
|
||||||
|
|||||||
47
app/models/beatmap.py
Normal file
47
app/models/beatmap.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class BeatmapRankStatus(IntEnum):
|
||||||
|
GRAVEYARD = -2
|
||||||
|
WIP = -1
|
||||||
|
PENDING = 0
|
||||||
|
RANKED = 1
|
||||||
|
APPROVED = 2
|
||||||
|
QUALIFIED = 3
|
||||||
|
LOVED = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Genre(IntEnum):
|
||||||
|
ANY = 0
|
||||||
|
UNSPECIFIED = 1
|
||||||
|
VIDEO_GAME = 2
|
||||||
|
ANIME = 3
|
||||||
|
ROCK = 4
|
||||||
|
POP = 5
|
||||||
|
OTHER = 6
|
||||||
|
NOVELTY = 7
|
||||||
|
HIP_HOP = 9
|
||||||
|
ELECTRONIC = 10
|
||||||
|
METAL = 11
|
||||||
|
CLASSICAL = 12
|
||||||
|
FOLK = 13
|
||||||
|
JAZZ = 14
|
||||||
|
|
||||||
|
|
||||||
|
class Language(IntEnum):
|
||||||
|
ANY = 0
|
||||||
|
UNSPECIFIED = 1
|
||||||
|
ENGLISH = 2
|
||||||
|
JAPANESE = 3
|
||||||
|
CHINESE = 4
|
||||||
|
INSTRUMENTAL = 5
|
||||||
|
KOREAN = 6
|
||||||
|
FRENCH = 7
|
||||||
|
GERMAN = 8
|
||||||
|
ITALIAN = 9
|
||||||
|
SPANISH = 10
|
||||||
|
RUSSIAN = 11
|
||||||
|
POLISH = 12
|
||||||
|
OTHER = 13
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class GameMode(str, Enum):
|
class GameMode(str, Enum):
|
||||||
OSU = "osu"
|
OSU = "osu"
|
||||||
TAIKO = "taiko"
|
TAIKO = "taiko"
|
||||||
FRUITS = "fruits"
|
FRUITS = "fruits"
|
||||||
MANIA = "mania"
|
MANIA = "mania"
|
||||||
|
|
||||||
|
|
||||||
|
MODE_TO_INT = {
|
||||||
|
GameMode.OSU: 0,
|
||||||
|
GameMode.TAIKO: 1,
|
||||||
|
GameMode.FRUITS: 2,
|
||||||
|
GameMode.MANIA: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIMod(BaseModel):
|
class APIMod(BaseModel):
|
||||||
acronym: str
|
acronym: str
|
||||||
settings: dict[str, Any] = {}
|
settings: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
|
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
|
||||||
class HitResult(IntEnum):
|
class HitResult(IntEnum):
|
||||||
PERFECT = 0 # [Order(0)]
|
PERFECT = 0 # [Order(0)]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from . import me # pyright: ignore[reportUnusedImport] # noqa: F401
|
from . import ( # pyright: ignore[reportUnusedImport] # noqa: F401
|
||||||
|
beatmap,
|
||||||
|
beatmapset,
|
||||||
|
me,
|
||||||
|
)
|
||||||
from .api_router import router as api_router
|
from .api_router import router as api_router
|
||||||
from .auth import router as auth_router
|
from .auth import router as auth_router
|
||||||
from .signalr import signalr_router as signalr_router
|
from .signalr import signalr_router as signalr_router
|
||||||
|
|
||||||
|
__all__ = ["api_router", "auth_router", "signalr_router"]
|
||||||
|
|||||||
26
app/router/beatmap.py
Normal file
26
app/router/beatmap.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.database import (
|
||||||
|
Beatmap,
|
||||||
|
BeatmapResp,
|
||||||
|
User as DBUser,
|
||||||
|
)
|
||||||
|
from app.dependencies.database import get_db
|
||||||
|
from app.dependencies.user import get_current_user
|
||||||
|
|
||||||
|
from .api_router import router
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/beatmaps/{bid}", tags=["beatmap"], response_model=BeatmapResp)
|
||||||
|
async def get_beatmap(
|
||||||
|
bid: int,
|
||||||
|
current_user: DBUser = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
beatmap = db.exec(select(Beatmap).where(Beatmap.id == bid)).first()
|
||||||
|
if not beatmap:
|
||||||
|
raise HTTPException(status_code=404, detail="Beatmap not found")
|
||||||
|
return BeatmapResp.from_db(beatmap)
|
||||||
26
app/router/beatmapset.py
Normal file
26
app/router/beatmapset.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.database import (
|
||||||
|
Beatmapset,
|
||||||
|
BeatmapsetResp,
|
||||||
|
User as DBUser,
|
||||||
|
)
|
||||||
|
from app.dependencies.database import get_db
|
||||||
|
from app.dependencies.user import get_current_user
|
||||||
|
|
||||||
|
from .api_router import router
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/beatmapsets/{sid}", tags=["beatmapset"], response_model=BeatmapsetResp)
|
||||||
|
async def get_beatmapset(
|
||||||
|
sid: int,
|
||||||
|
current_user: DBUser = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
beatmapset = db.exec(select(Beatmapset).where(Beatmapset.id == sid)).first()
|
||||||
|
if not beatmapset:
|
||||||
|
raise HTTPException(status_code=404, detail="Beatmapset not found")
|
||||||
|
return BeatmapsetResp.from_db(beatmapset)
|
||||||
4
main.py
4
main.py
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
from app.dependencies.database import engine
|
||||||
from app.router import api_router, auth_router, signalr_router
|
from app.router import api_router, auth_router, signalr_router
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
# 注意: 表结构现在通过 migrations 管理,不再自动创建
|
# 注意: 表结构现在通过 migrations 管理,不再自动创建
|
||||||
# 如需创建表,请运行: python quick_sync.py
|
# 如需创建表,请运行: python quick_sync.py
|
||||||
@@ -15,6 +17,8 @@ app.include_router(api_router, prefix="/api/v2")
|
|||||||
app.include_router(signalr_router, prefix="/signalr")
|
app.include_router(signalr_router, prefix="/signalr")
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|
||||||
|
SQLModel.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ def main():
|
|||||||
print("身份验证失败,请检查用户名和密码")
|
print("身份验证失败,请检查用户名和密码")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"访问令牌: {token_data['access_token'][:50]}...")
|
print(f"访问令牌: {token_data['access_token']}")
|
||||||
print(f"刷新令牌: {token_data['refresh_token'][:30]}...")
|
print(f"刷新令牌: {token_data['refresh_token']}")
|
||||||
print(f"令牌有效期: {token_data['expires_in']} 秒")
|
print(f"令牌有效期: {token_data['expires_in']} 秒")
|
||||||
|
|
||||||
# 4. 获取用户数据
|
# 4. 获取用户数据
|
||||||
@@ -153,7 +153,7 @@ def main():
|
|||||||
print("\n5. 测试令牌刷新...")
|
print("\n5. 测试令牌刷新...")
|
||||||
new_token_data = refresh_token(token_data["refresh_token"])
|
new_token_data = refresh_token(token_data["refresh_token"])
|
||||||
if new_token_data:
|
if new_token_data:
|
||||||
print(f"新访问令牌: {new_token_data['access_token'][:50]}...")
|
print(f"新访问令牌: {new_token_data['access_token']}")
|
||||||
|
|
||||||
# 使用新令牌获取用户数据
|
# 使用新令牌获取用户数据
|
||||||
print("\n6. 使用新令牌获取用户数据...")
|
print("\n6. 使用新令牌获取用户数据...")
|
||||||
|
|||||||
Reference in New Issue
Block a user