From 4f3ab38454d9ad2c989e1af0e5d8606fbab1e0ca Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Wed, 6 Aug 2025 12:07:12 +0000 Subject: [PATCH] feat(beatmap): support playcount & passcount --- app/database/__init__.py | 3 ++ app/database/beatmap.py | 27 +++++++++-- app/database/beatmap_playcounts.py | 77 ++++++++++++++++++++++++++++++ app/database/beatmapset.py | 2 +- app/database/score.py | 2 + app/router/beatmapset.py | 8 ++-- 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 app/database/beatmap_playcounts.py diff --git a/app/database/__init__.py b/app/database/__init__.py index 6e2e8c5..568104b 100644 --- a/app/database/__init__.py +++ b/app/database/__init__.py @@ -4,6 +4,7 @@ from .beatmap import ( Beatmap as Beatmap, BeatmapResp as BeatmapResp, ) +from .beatmap_playcounts import BeatmapPlaycounts, BeatmapPlaycountsResp from .beatmapset import ( Beatmapset as Beatmapset, BeatmapsetResp as BeatmapsetResp, @@ -37,6 +38,8 @@ from .user_account_history import ( __all__ = [ "Beatmap", + "BeatmapPlaycounts", + "BeatmapPlaycountsResp", "Beatmapset", "BeatmapsetResp", "BestScore", diff --git a/app/database/beatmap.py b/app/database/beatmap.py index c55643a..5470277 100644 --- a/app/database/beatmap.py +++ b/app/database/beatmap.py @@ -5,10 +5,11 @@ from app.models.beatmap import BeatmapRankStatus from app.models.model import UTCBaseModel from app.models.score import MODE_TO_INT, GameMode +from .beatmap_playcounts import BeatmapPlaycounts from .beatmapset import Beatmapset, BeatmapsetResp from sqlalchemy import DECIMAL, Column, DateTime -from sqlmodel import VARCHAR, Field, Relationship, SQLModel, select +from sqlmodel import VARCHAR, Field, Relationship, SQLModel, col, func, select from sqlmodel.ext.asyncio.session import AsyncSession if TYPE_CHECKING: @@ -58,8 +59,6 @@ class BeatmapBase(SQLModel, UTCBaseModel): 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): @@ -156,6 +155,8 @@ class BeatmapResp(BeatmapBase): mode_int: int ranked: int url: str = "" + playcount: int = 0 + passcount: int = 0 @classmethod async def from_db( @@ -166,6 +167,8 @@ class BeatmapResp(BeatmapBase): session: AsyncSession | None = None, user: "User | None" = None, ) -> "BeatmapResp": + from .score import Score + beatmap_ = beatmap.model_dump() if query_mode is not None and beatmap.mode != query_mode: beatmap_["convert"] = True @@ -177,4 +180,22 @@ class BeatmapResp(BeatmapBase): beatmap_["beatmapset"] = await BeatmapsetResp.from_db( beatmap.beatmapset, session=session, user=user ) + if session: + beatmap_["playcount"] = ( + await session.exec( + select(func.count()) + .select_from(BeatmapPlaycounts) + .where(BeatmapPlaycounts.beatmap_id == beatmap.id) + ) + ).one() + beatmap_["passcount"] = ( + await session.exec( + select(func.count()) + .select_from(Score) + .where( + Score.beatmap_id == beatmap.id, + col(Score.passed).is_(True), + ) + ) + ).one() return cls.model_validate(beatmap_) diff --git a/app/database/beatmap_playcounts.py b/app/database/beatmap_playcounts.py new file mode 100644 index 0000000..e8f4741 --- /dev/null +++ b/app/database/beatmap_playcounts.py @@ -0,0 +1,77 @@ +from typing import TYPE_CHECKING + +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlmodel import ( + BigInteger, + Column, + Field, + ForeignKey, + Relationship, + SQLModel, + select, +) +from sqlmodel.ext.asyncio.session import AsyncSession + +if TYPE_CHECKING: + from .beatmap import Beatmap, BeatmapResp + from .beatmapset import BeatmapsetResp + from .lazer_user import User + + +class BeatmapPlaycounts(AsyncAttrs, SQLModel, table=True): + __tablename__ = "beatmap_playcounts" # pyright: ignore[reportAssignmentType] + + id: int | None = Field( + default=None, + sa_column=Column(BigInteger, primary_key=True, autoincrement=True), + ) + user_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True) + ) + beatmap_id: int = Field(foreign_key="beatmaps.id", index=True) + playcount: int = Field(default=0) + + user: "User" = Relationship() + beatmap: "Beatmap" = Relationship() + + +class BeatmapPlaycountsResp(BaseModel): + beatmap_id: int + beatmap: "BeatmapResp | None" = None + beatmapset: "BeatmapsetResp | None" = None + count: int + + @classmethod + async def from_db(cls, db_model: BeatmapPlaycounts) -> "BeatmapPlaycountsResp": + from .beatmap import BeatmapResp + from .beatmapset import BeatmapsetResp + + await db_model.awaitable_attrs.beatmap + return cls( + beatmap_id=db_model.beatmap_id, + count=db_model.playcount, + beatmap=await BeatmapResp.from_db(db_model.beatmap), + beatmapset=await BeatmapsetResp.from_db(db_model.beatmap.beatmapset), + ) + + +async def process_beatmap_playcount( + session: AsyncSession, user_id: int, beatmap_id: int +) -> None: + existing_playcount = ( + await session.exec( + select(BeatmapPlaycounts).where( + BeatmapPlaycounts.user_id == user_id, + BeatmapPlaycounts.beatmap_id == beatmap_id, + ) + ) + ).first() + + if existing_playcount: + existing_playcount.playcount += 1 + else: + new_playcount = BeatmapPlaycounts( + user_id=user_id, beatmap_id=beatmap_id, playcount=1 + ) + session.add(new_playcount) diff --git a/app/database/beatmapset.py b/app/database/beatmapset.py index 49313b2..65a3b2a 100644 --- a/app/database/beatmapset.py +++ b/app/database/beatmapset.py @@ -217,7 +217,7 @@ class BeatmapsetResp(BeatmapsetBase): update = { "beatmaps": [ - await BeatmapResp.from_db(beatmap, from_set=True) + await BeatmapResp.from_db(beatmap, from_set=True, session=session) for beatmap in await beatmapset.awaitable_attrs.beatmaps ], "hype": BeatmapHype( diff --git a/app/database/score.py b/app/database/score.py index 79cb005..bbef7ab 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -28,6 +28,7 @@ from app.models.score import ( ) from .beatmap import Beatmap, BeatmapResp +from .beatmap_playcounts import process_beatmap_playcount from .beatmapset import BeatmapsetResp from .best_score import BestScore from .lazer_user import User, UserResp @@ -601,6 +602,7 @@ async def process_user( statistics.hit_accuracy = acc_sum if add_to_db: session.add(mouthly_playcount) + await process_beatmap_playcount(session, user.id, score.beatmap_id) await session.commit() await session.refresh(user) diff --git a/app/router/beatmapset.py b/app/router/beatmapset.py index b4d2e4c..c02b559 100644 --- a/app/router/beatmapset.py +++ b/app/router/beatmapset.py @@ -71,10 +71,10 @@ async def favourite_beatmapset( ) ).first() - if action == "favourite" and existing_favourite: - raise HTTPException(status_code=400, detail="Already favourited") - elif action == "unfavourite" and not existing_favourite: - raise HTTPException(status_code=400, detail="Not favourited") + if (action == "favourite" and existing_favourite) or ( + action == "unfavourite" and not existing_favourite + ): + return if action == "favourite": favourite = FavouriteBeatmapset(