1
0
mirror of synced 2025-02-22 04:39:37 +01:00
artemis/titles/mai2/schema/profile.py
beerpsi 58a5177a30 use SQL's limit/offset pagination for nextIndex/maxCount requests (#185)
Instead of retrieving the entire list of items/characters/scores/etc. at once (and even store them in memory), use SQL's `LIMIT ... OFFSET ...` pagination so we only take what we need.

Currently only CHUNITHM uses this, but this will also affect maimai DX and O.N.G.E.K.I. once the PR is ready.

Also snuck in a fix for CHUNITHM/maimai DX's `GetUserRivalMusicApi` to respect the `userRivalMusicLevelList` sent by the client.

### How this works

Say we have a `GetUserCharacterApi` request:

```json
{
    "userId": 10000,
    "maxCount": 700,
    "nextIndex": 0
}
```

Instead of getting the entire character list from the database (which can be very large if the user force unlocked everything), add limit/offset to the query:

```python
select(character)
.where(character.c.user == user_id)
.order_by(character.c.id.asc())
.limit(max_count + 1)
.offset(next_index)
```

The query takes `maxCount + 1` items from the database to determine if there is more items than can be returned:

```python
rows = ...

if len(rows) > max_count:
    # return only max_count rows
    next_index += max_count
else:
    # return everything left
    next_index = -1
```

This has the benefit of not needing to load everything into memory (and also having to store server state, as seen in the [`SCORE_BUFFER` list](2274b42358/titles/chuni/base.py (L13)).)

Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/185
Co-authored-by: beerpsi <beerpsi@duck.com>
Co-committed-by: beerpsi <beerpsi@duck.com>
2024-11-16 19:10:29 +00:00

1036 lines
35 KiB
Python

from datetime import datetime
from typing import Dict, List, Optional
from uuid import uuid4
from sqlalchemy import Column, Table, UniqueConstraint, and_
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.types import (
INTEGER,
JSON,
TIMESTAMP,
VARCHAR,
BigInteger,
Boolean,
Integer,
SmallInteger,
String,
)
from core.data.schema import BaseData, metadata
from titles.mai2.const import Mai2Constants
detail = Table(
"mai2_profile_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("userName", String(25)),
Column("isNetMember", Integer),
Column("iconId", Integer),
Column("plateId", Integer),
Column("titleId", Integer),
Column("partnerId", Integer),
Column("frameId", Integer),
Column("selectMapId", Integer),
Column("totalAwake", Integer),
Column("gradeRating", Integer),
Column("musicRating", Integer),
Column("playerRating", Integer),
Column("highestRating", Integer),
Column("gradeRank", Integer),
Column("classRank", Integer),
Column("courseRank", Integer),
Column("charaSlot", JSON),
Column("charaLockSlot", JSON),
Column("contentBit", BigInteger),
Column("playCount", Integer),
Column("currentPlayCount", Integer), # new with buddies
Column("renameCredit", Integer), # new with buddies
Column("mapStock", Integer), # new with fes+
Column("point", Integer), # new with buddies+
Column("totalPoint", Integer), # new with buddies+
Column("eventWatchedDate", String(25)),
Column("lastGameId", String(25)),
Column("lastRomVersion", String(25)),
Column("lastDataVersion", String(25)),
Column("lastLoginDate", String(25)),
Column("lastPairLoginDate", String(25)), # new with uni+
Column("lastPlayDate", String(25)),
Column("lastTrialPlayDate", String(25)), # new with uni+
Column("lastPlayCredit", Integer),
Column("lastPlayMode", Integer),
Column("lastPlaceId", Integer),
Column("lastPlaceName", String(25)),
Column("lastAllNetId", Integer),
Column("lastRegionId", Integer),
Column("lastRegionName", String(25)),
Column("lastClientId", String(25)),
Column("lastCountryCode", String(25)),
Column("lastSelectEMoney", Integer),
Column("lastSelectTicket", Integer),
Column("lastSelectCourse", Integer),
Column("lastCountCourse", Integer),
Column("firstGameId", String(25)),
Column("firstRomVersion", String(25)),
Column("firstDataVersion", String(25)),
Column("firstPlayDate", String(25)),
Column("compatibleCmVersion", String(25)),
Column("dailyBonusDate", String(25)),
Column("dailyCourseBonusDate", String(25)),
Column("playVsCount", Integer),
Column("playSyncCount", Integer),
Column("winCount", Integer),
Column("helpCount", Integer),
Column("comboCount", Integer),
Column("totalDeluxscore", BigInteger),
Column("totalBasicDeluxscore", BigInteger),
Column("totalAdvancedDeluxscore", BigInteger),
Column("totalExpertDeluxscore", BigInteger),
Column("totalMasterDeluxscore", BigInteger),
Column("totalReMasterDeluxscore", BigInteger),
Column("totalSync", Integer),
Column("totalBasicSync", Integer),
Column("totalAdvancedSync", Integer),
Column("totalExpertSync", Integer),
Column("totalMasterSync", Integer),
Column("totalReMasterSync", Integer),
Column("totalAchievement", BigInteger),
Column("totalBasicAchievement", BigInteger),
Column("totalAdvancedAchievement", BigInteger),
Column("totalExpertAchievement", BigInteger),
Column("totalMasterAchievement", BigInteger),
Column("totalReMasterAchievement", BigInteger),
Column("playerOldRating", BigInteger),
Column("playerNewRating", BigInteger),
Column("dateTime", BigInteger),
Column("friendRegistSkip", SmallInteger), # new with buddies+
Column("banState", Integer), # new with uni+
UniqueConstraint("user", "version", name="mai2_profile_detail_uk"),
mysql_charset="utf8mb4",
)
detail_old = Table(
"maimai_profile_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("lastDataVersion", Integer),
Column("userName", String(8)),
Column("point", Integer),
Column("totalPoint", Integer),
Column("iconId", Integer),
Column("nameplateId", Integer),
Column("frameId", Integer),
Column("trophyId", Integer),
Column("playCount", Integer),
Column("playVsCount", Integer),
Column("playSyncCount", Integer),
Column("winCount", Integer),
Column("helpCount", Integer),
Column("comboCount", Integer),
Column("feverCount", Integer),
Column("totalHiScore", Integer),
Column("totalEasyHighScore", Integer),
Column("totalBasicHighScore", Integer),
Column("totalAdvancedHighScore", Integer),
Column("totalExpertHighScore", Integer),
Column("totalMasterHighScore", Integer),
Column("totalReMasterHighScore", Integer),
Column("totalHighSync", Integer),
Column("totalEasySync", Integer),
Column("totalBasicSync", Integer),
Column("totalAdvancedSync", Integer),
Column("totalExpertSync", Integer),
Column("totalMasterSync", Integer),
Column("totalReMasterSync", Integer),
Column("playerRating", Integer),
Column("highestRating", Integer),
Column("rankAuthTailId", Integer),
Column("eventWatchedDate", String(255)),
Column("webLimitDate", String(255)),
Column("challengeTrackPhase", Integer),
Column("firstPlayBits", Integer),
Column("lastPlayDate", String(255)),
Column("lastPlaceId", Integer),
Column("lastPlaceName", String(255)),
Column("lastRegionId", Integer),
Column("lastRegionName", String(255)),
Column("lastClientId", String(255)),
Column("lastCountryCode", String(255)),
Column("eventPoint", Integer),
Column("totalLv", Integer),
Column("lastLoginBonusDay", Integer),
Column("lastSurvivalBonusDay", Integer),
Column("loginBonusLv", Integer),
UniqueConstraint("user", "version", name="maimai_profile_detail_uk"),
mysql_charset="utf8mb4",
)
ghost = Table(
"mai2_profile_ghost",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version_int", Integer, nullable=False),
Column("name", String(25)),
Column("iconId", Integer),
Column("plateId", Integer),
Column("titleId", Integer),
Column("rate", Integer),
Column("udemaeRate", Integer),
Column("courseRank", Integer),
Column("classRank", Integer),
Column("classValue", Integer),
Column("playDatetime", String(25)),
Column("shopId", Integer),
Column("regionCode", Integer),
Column("typeId", Integer),
Column("musicId", Integer),
Column("difficulty", Integer),
Column("version", Integer),
Column("resultBitList", JSON),
Column("resultNum", Integer),
Column("achievement", Integer),
UniqueConstraint(
"user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk"
),
mysql_charset="utf8mb4",
)
extend = Table(
"mai2_profile_extend",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("selectMusicId", Integer),
Column("selectDifficultyId", Integer),
Column("categoryIndex", Integer),
Column("musicIndex", Integer),
Column("extraFlag", Integer),
Column("selectScoreType", Integer),
Column("extendContentBit", BigInteger),
Column("isPhotoAgree", Boolean),
Column("isGotoCodeRead", Boolean),
Column("selectResultDetails", Boolean),
Column("selectResultScoreViewType", Integer), # new with fes+
Column("sortCategorySetting", Integer),
Column("sortMusicSetting", Integer),
Column("selectedCardList", JSON),
Column("encountMapNpcList", JSON),
Column("playStatusSetting", Integer, server_default="0"),
UniqueConstraint("user", "version", name="mai2_profile_extend_uk"),
mysql_charset="utf8mb4",
)
option = Table(
"mai2_profile_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("selectMusicId", Integer),
Column("optionKind", Integer),
Column("noteSpeed", Integer),
Column("slideSpeed", Integer),
Column("touchSpeed", Integer),
Column("tapDesign", Integer),
Column("tapSe", Integer, server_default="0"),
Column("holdDesign", Integer),
Column("slideDesign", Integer),
Column("starType", Integer),
Column("outlineDesign", Integer),
Column("noteSize", Integer),
Column("slideSize", Integer),
Column("touchSize", Integer),
Column("starRotate", Integer),
Column("dispCenter", Integer),
Column("outFrameType", Integer), # new with fes+
Column("dispChain", Integer),
Column("dispRate", Integer),
Column("dispBar", Integer),
Column("touchEffect", Integer),
Column("submonitorAnimation", Integer),
Column("submonitorAchive", Integer),
Column("submonitorAppeal", Integer),
Column("matching", Integer),
Column("trackSkip", Integer),
Column("brightness", Integer),
Column("mirrorMode", Integer),
Column("dispJudge", Integer),
Column("dispJudgePos", Integer),
Column("dispJudgeTouchPos", Integer),
Column("adjustTiming", Integer),
Column("judgeTiming", Integer),
Column("ansVolume", Integer),
Column("tapHoldVolume", Integer),
Column("criticalSe", Integer),
Column("breakSe", Integer),
Column("breakVolume", Integer),
Column("exSe", Integer),
Column("exVolume", Integer),
Column("slideSe", Integer),
Column("slideVolume", Integer),
Column("breakSlideVolume", Integer), # new with fes+
Column("touchVolume", Integer), # new with fes+
Column("touchHoldVolume", Integer),
Column("damageSeVolume", Integer),
Column("headPhoneVolume", Integer),
Column("sortTab", Integer),
Column("sortMusic", Integer),
UniqueConstraint("user", "version", name="mai2_profile_option_uk"),
mysql_charset="utf8mb4",
)
option_old = Table(
"maimai_profile_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("soudEffect", Integer),
Column("mirrorMode", Integer),
Column("guideSpeed", Integer),
Column("bgInfo", Integer),
Column("brightness", Integer),
Column("isStarRot", Integer),
Column("breakSe", Integer),
Column("slideSe", Integer),
Column("hardJudge", Integer),
Column("isTagJump", Integer),
Column("breakSeVol", Integer),
Column("slideSeVol", Integer),
Column("isUpperDisp", Integer),
Column("trackSkip", Integer),
Column("optionMode", Integer),
Column("simpleOptionParam", Integer),
Column("adjustTiming", Integer),
Column("dispTiming", Integer),
Column("timingPos", Integer),
Column("ansVol", Integer),
Column("noteVol", Integer),
Column("dmgVol", Integer),
Column("appealFlame", Integer),
Column("isFeverDisp", Integer),
Column("dispJudge", Integer),
Column("judgePos", Integer),
Column("ratingGuard", Integer),
Column("selectChara", Integer),
Column("sortType", Integer),
Column("filterGenre", Integer),
Column("filterLevel", Integer),
Column("filterRank", Integer),
Column("filterVersion", Integer),
Column("filterRec", Integer),
Column("filterFullCombo", Integer),
Column("filterAllPerfect", Integer),
Column("filterDifficulty", Integer),
Column("filterFullSync", Integer),
Column("filterReMaster", Integer),
Column("filterMaxFever", Integer),
Column("finalSelectId", Integer),
Column("finalSelectCategory", Integer),
UniqueConstraint("user", "version", name="maimai_profile_option_uk"),
mysql_charset="utf8mb4",
)
web_opt = Table(
"maimai_profile_web_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("isNetMember", Boolean),
Column("dispRate", Integer),
Column("dispJudgeStyle", Integer),
Column("dispRank", Integer),
Column("dispHomeRanker", Integer),
Column("dispTotalLv", Integer),
UniqueConstraint("user", "version", name="maimai_profile_web_option_uk"),
mysql_charset="utf8mb4",
)
grade_status = Table(
"maimai_profile_grade_status",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("gradeVersion", Integer),
Column("gradeLevel", Integer),
Column("gradeSubLevel", Integer),
Column("gradeMaxId", Integer),
UniqueConstraint("user", "gradeVersion", name="maimai_profile_grade_status_uk"),
mysql_charset="utf8mb4",
)
rating = Table(
"mai2_profile_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("rating", Integer),
Column("ratingList", JSON),
Column("newRatingList", JSON),
Column("nextRatingList", JSON),
Column("nextNewRatingList", JSON),
Column("udemae", JSON),
UniqueConstraint("user", "version", name="mai2_profile_rating_uk"),
mysql_charset="utf8mb4",
)
region = Table(
"mai2_profile_region",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("regionId", Integer),
Column("playCount", Integer, server_default="1"),
Column("created", String(25)),
UniqueConstraint("user", "regionId", name="mai2_profile_region_uk"),
mysql_charset="utf8mb4",
)
activity = Table(
"mai2_profile_activity",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("kind", Integer),
Column("activityId", Integer),
Column("param1", Integer),
Column("param2", Integer),
Column("param3", Integer),
Column("param4", Integer),
Column("sortNumber", Integer),
UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"),
mysql_charset="utf8mb4",
)
boss = Table(
"maimai_profile_boss",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("pandoraFlagList0", Integer),
Column("pandoraFlagList1", Integer),
Column("pandoraFlagList2", Integer),
Column("pandoraFlagList3", Integer),
Column("pandoraFlagList4", Integer),
Column("pandoraFlagList5", Integer),
Column("pandoraFlagList6", Integer),
Column("emblemFlagList", Integer),
UniqueConstraint("user", name="mai2_profile_boss_uk"),
mysql_charset="utf8mb4",
)
recent_rating = Table(
"maimai_profile_recent_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userRecentRatingList", JSON),
UniqueConstraint("user", name="mai2_profile_recent_rating_uk"),
mysql_charset="utf8mb4",
)
consec_logins = Table(
"mai2_profile_consec_logins",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("logins", Integer),
UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"),
mysql_charset="utf8mb4",
)
rival: Table = Table(
"mai2_user_rival",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column(
"rival",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("show", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "rival", name="mai2_user_rival_uk"),
mysql_charset="utf8mb4",
)
intimacy = Table(
"mai2_user_intimate",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("partnerId", Integer, nullable=False),
Column("intimateLevel", Integer, nullable=False),
Column("intimateCountRewarded", Integer, nullable=False),
UniqueConstraint("user", "partnerId", name="mai2_user_intimate_uk"),
mysql_charset="utf8mb4",
)
photo = Table( # end-of-credit memorial photos, NOT user portraits
"mai2_user_photo",
metadata,
Column("id", VARCHAR(36), primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("playlog_num", INTEGER, nullable=False),
Column("track_num", INTEGER, nullable=False),
Column("when_upload", TIMESTAMP, nullable=False, server_default=func.now()),
UniqueConstraint("user", "playlog_num", "track_num", name="mai2_user_photo_uk"),
mysql_charset="utf8mb4",
)
class Mai2ProfileData(BaseData):
async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(detail.select(detail.c.user == user_id))
if result:
return result.fetchall()
async def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
) -> Optional[Row]:
detail_data["user"] = user_id
detail_data["version"] = version
if is_dx:
sql = insert(detail).values(**detail_data)
else:
sql = insert(detail_old).values(**detail_data)
conflict = sql.on_duplicate_key_update(**detail_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(
f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
async def get_profile_detail(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx:
sql = (
select(detail)
.where(and_(detail.c.user == user_id, detail.c.version <= version))
.order_by(detail.c.version.desc())
)
else:
sql = (
select(detail_old)
.where(
and_(detail_old.c.user == user_id, detail_old.c.version <= version)
)
.order_by(detail_old.c.version.desc())
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_profile_ghost(
self, user_id: int, version: int, ghost_data: Dict
) -> Optional[int]:
ghost_data["user"] = user_id
ghost_data["version_int"] = version
sql = insert(ghost).values(**ghost_data)
conflict = sql.on_duplicate_key_update(**ghost_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_profile_ghost: failed to update! {user_id}")
return None
return result.lastrowid
async def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]:
sql = (
select(ghost)
.where(and_(ghost.c.user == user_id, ghost.c.version_int <= version))
.order_by(ghost.c.version.desc())
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_profile_extend(
self, user_id: int, version: int, extend_data: Dict
) -> Optional[int]:
extend_data["user"] = user_id
extend_data["version"] = version
sql = insert(extend).values(**extend_data)
conflict = sql.on_duplicate_key_update(**extend_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_profile_extend: failed to update! {user_id}")
return None
return result.lastrowid
async def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]:
sql = (
select(extend)
.where(and_(extend.c.user == user_id, extend.c.version <= version))
.order_by(extend.c.version.desc())
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_profile_option(
self, user_id: int, version: int, option_data: Dict, is_dx: bool = True
) -> Optional[int]:
option_data["user"] = user_id
option_data["version"] = version
if is_dx:
sql = insert(option).values(**option_data)
else:
sql = insert(option_old).values(**option_data)
conflict = sql.on_duplicate_key_update(**option_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(
f"put_profile_option: failed to update! {user_id} is_dx {is_dx}"
)
return None
return result.lastrowid
async def get_profile_option(
self, user_id: int, version: int, is_dx: bool = True
) -> Optional[Row]:
if is_dx:
sql = (
select(option)
.where(and_(option.c.user == user_id, option.c.version <= version))
.order_by(option.c.version.desc())
)
else:
sql = (
select(option_old)
.where(
and_(option_old.c.user == user_id, option_old.c.version <= version)
)
.order_by(option_old.c.version.desc())
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_profile_rating(
self, user_id: int, version: int, rating_data: Dict
) -> Optional[int]:
rating_data["user"] = user_id
rating_data["version"] = version
sql = insert(rating).values(**rating_data)
conflict = sql.on_duplicate_key_update(**rating_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_profile_rating: failed to update! {user_id}")
return None
return result.lastrowid
async def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]:
sql = (
select(rating)
.where(and_(rating.c.user == user_id, rating.c.version <= version))
.order_by(rating.c.version.desc())
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]:
sql = insert(region).values(
user=user_id,
regionId=region_id,
created=datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT),
)
conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_region: failed to update! {user_id}")
return None
return result.lastrowid
async def get_regions(self, user_id: int) -> Optional[List[Dict]]:
sql = select(region).where(region.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]:
if "id" in activity_data:
activity_data["activityId"] = activity_data["id"]
activity_data.pop("id")
activity_data["user"] = user_id
sql = insert(activity).values(**activity_data)
conflict = sql.on_duplicate_key_update(**activity_data)
result = await self.execute(conflict)
if result is None:
self.logger.warning(
f"put_profile_activity: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
async def get_profile_activity(
self, user_id: int, kind: int = None
) -> Optional[List[Row]]:
sql = activity.select(
and_(
activity.c.user == user_id,
(activity.c.kind == kind) if kind is not None else True,
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def put_web_option(
self, user_id: int, version: int, web_opts: Dict
) -> Optional[int]:
web_opts["user"] = user_id
web_opts["version"] = version
sql = insert(web_opt).values(**web_opts)
conflict = sql.on_duplicate_key_update(**web_opts)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_web_option: failed to update! user_id: {user_id}")
return None
return result.lastrowid
async def get_web_option(self, user_id: int, version: int) -> Optional[Row]:
sql = web_opt.select(
and_(web_opt.c.user == user_id, web_opt.c.version == version)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]:
grade_stat["user"] = user_id
sql = insert(grade_status).values(**grade_stat)
conflict = sql.on_duplicate_key_update(**grade_stat)
result = await self.execute(conflict)
if result is None:
self.logger.warning(
f"put_grade_status: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
async def get_grade_status(self, user_id: int) -> Optional[Row]:
sql = grade_status.select(grade_status.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]:
boss_stat["user"] = user_id
sql = insert(boss).values(**boss_stat)
conflict = sql.on_duplicate_key_update(**boss_stat)
result = await self.execute(conflict)
if result is None:
self.logger.warning(f"put_boss_list: failed to update! user_id: {user_id}")
return None
return result.lastrowid
async def get_boss_list(self, user_id: int) -> Optional[Row]:
sql = boss.select(boss.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]:
sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr)
conflict = sql.on_duplicate_key_update({"userRecentRatingList": rr})
result = await self.execute(conflict)
if result is None:
self.logger.warning(
f"put_recent_rating: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
async def get_recent_rating(self, user_id: int) -> Optional[Row]:
sql = recent_rating.select(recent_rating.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def add_consec_login(self, user_id: int, version: int) -> None:
sql = insert(consec_logins).values(user=user_id, version=version, logins=1)
conflict = sql.on_duplicate_key_update(logins=consec_logins.c.logins + 1)
result = await self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update consecutive login count for user {user_id} version {version}"
)
async def get_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = select(consec_logins).where(
and_(
consec_logins.c.user == user_id,
consec_logins.c.version == version,
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = consec_logins.update(
and_(
consec_logins.c.user == user_id,
consec_logins.c.version == version,
)
).values(logins=1)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def get_rivals(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(rival.select(rival.c.user == user_id))
if result:
return result.fetchall()
async def get_rivals_game(
self,
user_id: int,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> Optional[List[Row]]:
sql = select(rival).where((rival.c.user == user_id) & rival.c.show.is_(True))
if limit is not None or offset is not None:
sql = sql.order_by(rival.c.id)
if limit is not None:
sql = sql.limit(limit)
if offset is not None:
sql = sql.offset(offset)
result = await self.execute(sql)
if result:
return result.fetchall()
async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None:
sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values(
show = is_shown
)
result = await self.execute(sql)
if not result:
self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}")
async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]:
sql = insert(rival).values(
user = user_id,
rival = rival_id
)
conflict = sql.on_duplicate_key_update(rival = rival_id)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!")
async def remove_rival(self, user_id: int, rival_id: int) -> None:
result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id)))
if not result:
self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!")
async def get_intimacy(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(intimacy.select(intimacy.c.user == user_id))
if result:
return result.fetchall()
async def put_intimacy(self, user_id: int, partner_id: int, level: int, count_rewarded: int) -> Optional[int]:
sql = insert(intimacy).values(
user = user_id,
partnerId = partner_id,
intimateLevel = level,
intimateCountRewarded = count_rewarded
)
conflict = sql.on_duplicate_key_update(intimateLevel = level, intimateCountRewarded = count_rewarded)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to update intimacy for user {user_id} and partner {partner_id}!")
async def put_user_photo(self, user_id: int, playlog_num: int, track_num: int) -> Optional[str]:
photo_id = str(uuid4())
sql = insert(photo).values(
id = photo_id,
user = user_id,
playlog_num = playlog_num,
track_num = track_num,
)
conflict = sql.on_duplicate_key_update(user = user_id)
result = await self.execute(conflict)
if result:
return photo_id
async def get_user_photo_by_id(self, photo_id: str) -> Optional[Row]:
result = await self.execute(photo.select(photo.c.id.like(photo_id)))
if result:
return result.fetchone()
async def get_user_photo_by_user_playlog_track(self, user_id: int, playlog_num: int, track_num: int) -> Optional[Row]:
result = await self.execute(photo.select(and_(and_(photo.c.user == user_id, photo.c.playlog_num == playlog_num), photo.c.track_num == track_num)))
if result:
return result.fetchone()
async def get_user_photos_by_user(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(photo.select(photo.c.user == user_id))
if result:
return result.fetchall()
async def delete_user_photo_by_id(self, photo_id: str) -> Optional[List[Row]]:
result = await self.execute(photo.delete(photo.c.id.like(photo_id)))
if not result:
self.logger.error(f"Failed to delete photo {photo_id}")
async def update_name(self, user_id: int, new_name: str) -> bool:
sql = detail.update(detail.c.user == user_id).values(
userName=new_name
)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Failed to set user {user_id} name to {new_name}")
return False
return True