feat(beatmap): support playcount & passcount

This commit is contained in:
MingxuanGame
2025-08-06 12:07:12 +00:00
parent 9f7ab81213
commit 4f3ab38454
6 changed files with 111 additions and 8 deletions

View File

@@ -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",

View File

@@ -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_)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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(