feat(performance-point): switch performance calculator to performance-server (#80)

* feat(config): make `performance_server` as default calculator

* deploy(docker): use osu-performance-server

* docs(readme): add ruleset download instructions

* chore(dev): update development environment

* feat(dev): update development environment setup and service startup order

* fix(deps): move `rosu-pp-py` to `project.optional-dependencies`

* feat(beatmap): handle deleted beatmaps

* feat(performance-server): add a long timeout for calculation

* feat(recalculate): enhance CLI arguments for performance, leaderboard, and rating recalculations with CSV output support

* fix(recalculate): resolve reviews

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(beatmapsync): resolve too long line

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
MingxuanGame
2025-11-09 01:59:09 +08:00
committed by GitHub
parent 293e57aea3
commit 0be3e903d4
20 changed files with 942 additions and 264 deletions

View File

@@ -83,7 +83,7 @@ class PerformanceServerPerformanceCalculator(BasePerformanceCalculator):
async def calculate_performance(self, beatmap_raw: str, score: "Score") -> PerformanceAttributes:
# https://github.com/GooGuTeam/osu-performance-server#post-performance
async with AsyncClient() as client:
async with AsyncClient(timeout=15) as client:
try:
resp = await client.post(
f"{self.server_url}/performance",
@@ -121,7 +121,7 @@ class PerformanceServerPerformanceCalculator(BasePerformanceCalculator):
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
) -> DifficultyAttributes:
# https://github.com/GooGuTeam/osu-performance-server#post-difficulty
async with AsyncClient() as client:
async with AsyncClient(timeout=15) as client:
try:
resp = await client.post(
f"{self.server_url}/difficulty",

View File

@@ -114,14 +114,7 @@ STORAGE_SETTINGS='{
""",
"表现计算设置": """配置表现分计算器及其参数。
### rosu-pp-py (默认)
```bash
CALCULATOR="rosu"
CALCULATOR_CONFIG='{}'
```
### [osu-performance-server](https://github.com/GooGuTeam/osu-performance-server)
### [osu-performance-server](https://github.com/GooGuTeam/osu-performance-server) (默认)
```bash
CALCULATOR="performance_server"
@@ -129,6 +122,13 @@ CALCULATOR_CONFIG='{
"server_url": "http://localhost:5225"
}'
```
### rosu-pp-py
```bash
CALCULATOR="rosu"
CALCULATOR_CONFIG='{}'
```
""",
}
},
@@ -533,13 +533,13 @@ CALCULATOR_CONFIG='{
# 表现计算设置
calculator: Annotated[
Literal["rosu", "performance_server"],
Field(default="rosu", description="表现分计算器"),
Field(default="performance_server", description="表现分计算器"),
"表现计算设置",
]
calculator_config: Annotated[
dict[str, Any],
Field(
default={},
default={"server_url": "http://localhost:5225"},
description="表现分计算器配置 (JSON 格式),具体配置项请参考上方",
),
"表现计算设置",

View File

@@ -160,6 +160,7 @@ class BeatmapResp(BeatmapBase):
failtimes: FailTimeResp | None = None
top_tag_ids: list[APIBeatmapTag] | None = None
current_user_tag_ids: list[int] | None = None
is_deleted: bool = False
@classmethod
async def from_db(
@@ -184,6 +185,7 @@ class BeatmapResp(BeatmapBase):
beatmap_["status"] = beatmap_status.name.lower()
beatmap_["ranked"] = beatmap_status.value
beatmap_["mode_int"] = int(beatmap.mode)
beatmap_["is_deleted"] = beatmap.deleted_at is not None
if not from_set:
beatmap_["beatmapset"] = await BeatmapsetResp.from_db(beatmap.beatmapset, session=session, user=user)
if beatmap.failtimes is not None:

View File

@@ -23,11 +23,13 @@ class BeatmapRawFetcher(BaseFetcher):
resp = await self._request(req_url)
if resp.status_code >= 400:
continue
if not resp.text:
continue
return resp.text
raise HTTPError("Failed to fetch beatmap")
async def _request(self, url: str) -> Response:
async with AsyncClient() as client:
async with AsyncClient(timeout=15) as client:
response = await client.get(
url,
)

View File

@@ -182,6 +182,7 @@ class BeatmapsetUpdateService:
logger.error(f"failed to add missing beatmapset {missing}: {e}")
if total > 0:
logger.opt(colors=True).info(f"added {total} missing beatmapset")
await session.commit()
self._adding_missing = False
async def add(self, beatmapset: BeatmapsetResp, calculate_next_sync: bool = True):
@@ -397,7 +398,15 @@ class BeatmapsetUpdateService:
existing_beatmap = await session.get(Beatmap, change.beatmap_id)
if existing_beatmap:
await session.merge(new_db_beatmap)
if change.type == BeatmapChangeType.MAP_DELETED:
existing_beatmap.deleted_at = utcnow()
await session.commit()
else:
if change.type == BeatmapChangeType.MAP_DELETED:
logger.opt(colors=True).warning(
f"<g>[beatmap: {change.beatmap_id}]</g> MAP_DELETED received "
f"but beatmap not found in database; deletion skipped"
)
if change.type != BeatmapChangeType.STATUS_CHANGED:
await _process_update_or_delete_beatmaps(change.beatmap_id)
await get_beatmapset_cache_service(get_redis()).invalidate_beatmap_lookup_cache(change.beatmap_id)