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, Beatmap as Beatmap,
BeatmapResp as BeatmapResp, BeatmapResp as BeatmapResp,
) )
from .beatmap_playcounts import BeatmapPlaycounts, BeatmapPlaycountsResp
from .beatmapset import ( from .beatmapset import (
Beatmapset as Beatmapset, Beatmapset as Beatmapset,
BeatmapsetResp as BeatmapsetResp, BeatmapsetResp as BeatmapsetResp,
@@ -37,6 +38,8 @@ from .user_account_history import (
__all__ = [ __all__ = [
"Beatmap", "Beatmap",
"BeatmapPlaycounts",
"BeatmapPlaycountsResp",
"Beatmapset", "Beatmapset",
"BeatmapsetResp", "BeatmapsetResp",
"BestScore", "BestScore",

View File

@@ -5,10 +5,11 @@ from app.models.beatmap import BeatmapRankStatus
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.models.score import MODE_TO_INT, GameMode from app.models.score import MODE_TO_INT, GameMode
from .beatmap_playcounts import BeatmapPlaycounts
from .beatmapset import Beatmapset, BeatmapsetResp from .beatmapset import Beatmapset, BeatmapsetResp
from sqlalchemy import DECIMAL, Column, DateTime 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 from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -58,8 +59,6 @@ class BeatmapBase(SQLModel, UTCBaseModel):
deleted_at: datetime | None = Field(default=None, sa_column=Column(DateTime)) deleted_at: datetime | None = Field(default=None, sa_column=Column(DateTime))
hit_length: int = Field(default=0) hit_length: int = Field(default=0)
last_updated: datetime = Field(sa_column=Column(DateTime)) last_updated: datetime = Field(sa_column=Column(DateTime))
passcount: int = Field(default=0)
playcount: int = Field(default=0)
class Beatmap(BeatmapBase, table=True): class Beatmap(BeatmapBase, table=True):
@@ -156,6 +155,8 @@ class BeatmapResp(BeatmapBase):
mode_int: int mode_int: int
ranked: int ranked: int
url: str = "" url: str = ""
playcount: int = 0
passcount: int = 0
@classmethod @classmethod
async def from_db( async def from_db(
@@ -166,6 +167,8 @@ class BeatmapResp(BeatmapBase):
session: AsyncSession | None = None, session: AsyncSession | None = None,
user: "User | None" = None, user: "User | None" = None,
) -> "BeatmapResp": ) -> "BeatmapResp":
from .score import Score
beatmap_ = beatmap.model_dump() beatmap_ = beatmap.model_dump()
if query_mode is not None and beatmap.mode != query_mode: if query_mode is not None and beatmap.mode != query_mode:
beatmap_["convert"] = True beatmap_["convert"] = True
@@ -177,4 +180,22 @@ class BeatmapResp(BeatmapBase):
beatmap_["beatmapset"] = await BeatmapsetResp.from_db( beatmap_["beatmapset"] = await BeatmapsetResp.from_db(
beatmap.beatmapset, session=session, user=user 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_) 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 = { update = {
"beatmaps": [ "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 for beatmap in await beatmapset.awaitable_attrs.beatmaps
], ],
"hype": BeatmapHype( "hype": BeatmapHype(

View File

@@ -28,6 +28,7 @@ from app.models.score import (
) )
from .beatmap import Beatmap, BeatmapResp from .beatmap import Beatmap, BeatmapResp
from .beatmap_playcounts import process_beatmap_playcount
from .beatmapset import BeatmapsetResp from .beatmapset import BeatmapsetResp
from .best_score import BestScore from .best_score import BestScore
from .lazer_user import User, UserResp from .lazer_user import User, UserResp
@@ -601,6 +602,7 @@ async def process_user(
statistics.hit_accuracy = acc_sum statistics.hit_accuracy = acc_sum
if add_to_db: if add_to_db:
session.add(mouthly_playcount) session.add(mouthly_playcount)
await process_beatmap_playcount(session, user.id, score.beatmap_id)
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)

View File

@@ -71,10 +71,10 @@ async def favourite_beatmapset(
) )
).first() ).first()
if action == "favourite" and existing_favourite: if (action == "favourite" and existing_favourite) or (
raise HTTPException(status_code=400, detail="Already favourited") action == "unfavourite" and not existing_favourite
elif action == "unfavourite" and not existing_favourite: ):
raise HTTPException(status_code=400, detail="Not favourited") return
if action == "favourite": if action == "favourite":
favourite = FavouriteBeatmapset( favourite = FavouriteBeatmapset(