From a15c3cef04649b97a5cf9c33283a523f570bf716 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Thu, 31 Jul 2025 02:13:18 +0000 Subject: [PATCH] feat(user): add monthly playcounts --- app/database/lazer_user.py | 34 +++++++++++++++++++++-- app/database/monthly_playcounts.py | 43 ++++++++++++++++++++++++++++++ app/database/score.py | 23 +++++++++++++--- app/router/me.py | 11 ++------ app/router/user.py | 14 +++------- 5 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 app/database/monthly_playcounts.py diff --git a/app/database/lazer_user.py b/app/database/lazer_user.py index 9627015..9b98c98 100644 --- a/app/database/lazer_user.py +++ b/app/database/lazer_user.py @@ -7,6 +7,7 @@ from app.models.user import Country, Page, RankHistory from .achievement import UserAchievement, UserAchievementResp from .daily_challenge import DailyChallengeStats, DailyChallengeStatsResp +from .monthly_playcounts import MonthlyPlaycounts, MonthlyPlaycountsResp from .statistics import UserStatistics, UserStatisticsResp from .team import Team, TeamMember from .user_account_history import UserAccountHistory, UserAccountHistoryResp @@ -141,6 +142,7 @@ class User(UserBase, table=True): daily_challenge_stats: DailyChallengeStats | None = Relationship( back_populates="user" ) + monthly_playcounts: list[MonthlyPlaycounts] = Relationship(back_populates="user") email: str = Field(max_length=254, unique=True, index=True, exclude=True) priv: int = Field(default=1, exclude=True) @@ -160,6 +162,7 @@ class User(UserBase, table=True): selectinload(cls.achievement), # pyright: ignore[reportArgumentType] joinedload(cls.team_membership).joinedload(TeamMember.team), # pyright: ignore[reportArgumentType] joinedload(cls.daily_challenge_stats), # pyright: ignore[reportArgumentType] + selectinload(cls.monthly_playcounts), # pyright: ignore[reportArgumentType] ) @@ -186,7 +189,7 @@ class UserResp(UserBase): account_history: list[UserAccountHistoryResp] = [] active_tournament_banners: list[dict] = [] # TODO kudosu: Kudosu = Field(default_factory=lambda: Kudosu(available=0, total=0)) # TODO - monthly_playcounts: list = Field(default_factory=list) # TODO + monthly_playcounts: list[MonthlyPlaycountsResp] = Field(default_factory=list) unread_pm_count: int = 0 # TODO rank_history: RankHistory | None = None # TODO rank_highest: RankHighest | None = None # TODO @@ -196,7 +199,7 @@ class UserResp(UserBase): cover_url: str = "" # deprecated team: Team | None = None session_verified: bool = True - daily_challenge_user_stats: DailyChallengeStatsResp | None = None # TODO + daily_challenge_user_stats: DailyChallengeStatsResp | None = None # TODO: monthly_playcounts, unread_pm_count, rank_history, user_preferences @@ -292,9 +295,36 @@ class UserResp(UserBase): i.mode.value: UserStatisticsResp.from_db(i) for i in obj.statistics } + if "monthly_playcounts" in include: + u.monthly_playcounts = [ + MonthlyPlaycountsResp.from_db(pc) for pc in obj.monthly_playcounts + ] + if "achievements" in include: u.user_achievements = [ UserAchievementResp.from_db(ua) for ua in obj.achievement ] return u + + +ALL_INCLUDED = [ + "friends", + "team", + "account_history", + "daily_challenge_user_stats", + "statistics", + "statistics_rulesets", + "achievements", + "monthly_playcounts", +] + + +SEARCH_INCLUDED = [ + "team", + "daily_challenge_user_stats", + "statistics", + "statistics_rulesets", + "achievements", + "monthly_playcounts", +] diff --git a/app/database/monthly_playcounts.py b/app/database/monthly_playcounts.py new file mode 100644 index 0000000..46192d1 --- /dev/null +++ b/app/database/monthly_playcounts.py @@ -0,0 +1,43 @@ +from datetime import date +from typing import TYPE_CHECKING + +from sqlmodel import ( + BigInteger, + Column, + Field, + ForeignKey, + Relationship, + SQLModel, +) + +if TYPE_CHECKING: + from .lazer_user import User + + +class MonthlyPlaycounts(SQLModel, table=True): + __tablename__ = "monthly_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) + ) + year: int = Field(index=True) + month: int = Field(index=True) + playcount: int = Field(default=0) + + user: "User" = Relationship(back_populates="monthly_playcounts") + + +class MonthlyPlaycountsResp(SQLModel): + start_date: date + count: int + + @classmethod + def from_db(cls, db_model: MonthlyPlaycounts) -> "MonthlyPlaycountsResp": + return cls( + start_date=date(db_model.year, db_model.month, 1), + count=db_model.playcount, + ) diff --git a/app/database/score.py b/app/database/score.py index c805563..32c8cf5 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -1,6 +1,6 @@ import asyncio from collections.abc import Sequence -from datetime import UTC, datetime +from datetime import UTC, date, datetime import math from typing import TYPE_CHECKING @@ -30,6 +30,7 @@ from .beatmap import Beatmap, BeatmapResp from .beatmapset import Beatmapset, BeatmapsetResp from .best_score import BestScore from .lazer_user import User, UserResp +from .monthly_playcounts import MonthlyPlaycounts from .score_token import ScoreToken from redis import Redis @@ -501,8 +502,22 @@ async def process_user( previous_score_best = await get_user_best_score_in_beatmap( session, score.beatmap_id, user.id, score.gamemode ) - statistics = None add_to_db = False + mouthly_playcount = ( + await session.exec( + select(MonthlyPlaycounts).where( + MonthlyPlaycounts.user_id == user.id, + MonthlyPlaycounts.year == date.today().year, + MonthlyPlaycounts.month == date.today().month, + ) + ) + ).first() + if mouthly_playcount is None: + mouthly_playcount = MonthlyPlaycounts( + user_id=user.id, year=date.today().year, month=date.today().month + ) + add_to_db = True + statistics = None for i in user.statistics: if i.mode == score.gamemode.value: statistics = i @@ -547,6 +562,7 @@ async def process_user( statistics.level_current = calculate_score_to_level(statistics.ranked_score) statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo) statistics.play_count += 1 + mouthly_playcount.playcount += 1 statistics.play_time += int((score.ended_at - score.started_at).total_seconds()) statistics.count_100 += score.n100 + score.nkatu statistics.count_300 += score.n300 + score.ngeki @@ -569,9 +585,8 @@ async def process_user( acc_sum = clamp(acc_sum, 0.0, 100.0) statistics.pp = pp_sum statistics.hit_accuracy = acc_sum - if add_to_db: - session.add(statistics) + session.add(mouthly_playcount) await session.commit() await session.refresh(user) diff --git a/app/router/me.py b/app/router/me.py index e3aa734..b6d7d26 100644 --- a/app/router/me.py +++ b/app/router/me.py @@ -1,6 +1,7 @@ from __future__ import annotations from app.database import User, UserResp +from app.database.lazer_user import ALL_INCLUDED from app.dependencies import get_current_user from app.dependencies.database import get_db from app.models.score import GameMode @@ -21,14 +22,6 @@ async def get_user_info_default( return await UserResp.from_db( current_user, session, - [ - "friends", - "team", - "account_history", - "daily_challenge_user_stats", - "statistics", - "statistics_rulesets", - "achievements", - ], + ALL_INCLUDED, ruleset, ) diff --git a/app/router/user.py b/app/router/user.py index cfe136c..3df5a49 100644 --- a/app/router/user.py +++ b/app/router/user.py @@ -1,6 +1,7 @@ from __future__ import annotations from app.database import User, UserResp +from app.database.lazer_user import SEARCH_INCLUDED from app.dependencies.database import get_db from app.models.score import GameMode @@ -17,15 +18,6 @@ class BatchUserResponse(BaseModel): users: list[UserResp] -SEARCH_INCLUDE = [ - "team", - "daily_challenge_user_stats", - "statistics", - "statistics_rulesets", - "achievements", -] - - @router.get("/users", response_model=BatchUserResponse) @router.get("/users/lookup", response_model=BatchUserResponse) @router.get("/users/lookup/", response_model=BatchUserResponse) @@ -54,7 +46,7 @@ async def get_users( await UserResp.from_db( searched_user, session, - include=SEARCH_INCLUDE, + include=SEARCH_INCLUDED, ) for searched_user in searched_users ] @@ -85,6 +77,6 @@ async def get_user_info( return await UserResp.from_db( searched_user, session, - include=SEARCH_INCLUDE, + include=SEARCH_INCLUDED, ruleset=ruleset, )