1
0
mirror of synced 2025-02-16 02:12:32 +01:00
daydensteve b04840f3dd [chuni] Frontend favorites support (#176)
I had been itching for the favorites feature since I'm bad with japanese so figured I'd go ahead and add it. I've included a few pics to help visualize the changes.

### Summary of user-facing changes:
- New Favorites frontend page that itemizes favorites by genre for the current version (as selected on the Profile page). Favorites can be removed from this page via the Remove button
- Updated the Records page so that it only shows the playlog for the currently selected version and includes a "star" to the left of each title that can be clicked to add/remove favorites. When the star is yellow, its a favorite; when its a grey outline, its not. I figure its pretty straight forward
- The Records and new Favorites pages show the jacket image of each song now (The Importer was updated to convert the DDS files to PNGs on import)

### Behind-the-scenes changes:
- Fixed a bug in the chuni get_song method - it was inappropriately comparing the row id instead of the musicid (note this method was not used prior to adding favorites support)
- Overhauled the score scheme file to stop with all the hacky romVersion determination that was going on in various methods. To do this, I created a new ChuniRomVersion class that is populated with all base rom versions, then used to derive the internal integer version  number from the string stored in the DB. As written, this functionality can infer recorded rom versions when the playlog was entered using an update to the base version (e.g. 2.16  vs 2.15 for sunplus or 2.22 vs 2.20 for luminous).
- Made the chuni config version class safer as it would previously throw an exception if you gave it a version not present in the config file. This was done in support of the score overhaul to build up the initial ChuniRomVersion dict
- Added necessary methods to query/update the favorites table.

### Testing
- Frontend testing was performed with playlog data for both sunplus (2.16) and luminous (2.22) present. All add/remove permutations and images behavior was as expected
- Game testing was performed only with Luminous (2.22) and worked fine

Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/176
Co-authored-by: daydensteve <daydensteve@gmail.com>
Co-committed-by: daydensteve <daydensteve@gmail.com>
2024-09-25 14:53:43 +00:00

371 lines
14 KiB
Python

from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.sql.expression import exists
from core.data.schema import BaseData, metadata
from ..config import ChuniConfig
course = Table(
"chuni_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("classId", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Integer),
Column("scoreRank", Integer),
Column("eventId", Integer),
Column("lastPlayDate", String(25)),
Column("param1", Integer),
Column("param2", Integer),
Column("param3", Integer),
Column("param4", Integer),
Column("isClear", Integer),
Column("theoryCount", Integer),
Column("orderId", Integer),
Column("playerRating", Integer),
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
mysql_charset="utf8mb4",
)
best_score = Table(
"chuni_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("scoreMax", Integer),
Column("resRequestCount", Integer),
Column("resAcceptCount", Integer),
Column("resSuccessCount", Integer),
Column("missCount", Integer),
Column("maxComboCount", Integer),
Column("isFullCombo", Boolean),
Column("isAllJustice", Boolean),
Column("isSuccess", Integer),
Column("fullChain", Integer),
Column("maxChain", Integer),
Column("scoreRank", Integer),
Column("isLock", Boolean),
Column("ext1", Integer),
Column("theoryCount", Integer),
UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"),
mysql_charset="utf8mb4",
)
playlog = Table(
"chuni_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("playDate", String(20)),
Column("userPlayDate", String(20)),
Column("musicId", Integer),
Column("level", Integer),
Column("customId", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(20)),
Column("playedUserName2", String(20)),
Column("playedUserName3", String(20)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("playedCustom1", Integer),
Column("playedCustom2", Integer),
Column("playedCustom3", Integer),
Column("track", Integer),
Column("score", Integer),
Column("rank", Integer),
Column("maxCombo", Integer),
Column("maxChain", Integer),
Column("rateTap", Integer),
Column("rateHold", Integer),
Column("rateSlide", Integer),
Column("rateAir", Integer),
Column("rateFlick", Integer),
Column("judgeGuilty", Integer),
Column("judgeAttack", Integer),
Column("judgeJustice", Integer),
Column("judgeCritical", Integer),
Column("eventId", Integer),
Column("playerRating", Integer),
Column("isNewRecord", Boolean),
Column("isFullCombo", Boolean),
Column("fullChainKind", Integer),
Column("isAllJustice", Boolean),
Column("isContinue", Boolean),
Column("isFreeToPlay", Boolean),
Column("characterId", Integer),
Column("skillId", Integer),
Column("playKind", Integer),
Column("isClear", Integer),
Column("skillLevel", Integer),
Column("skillEffect", Integer),
Column("placeName", String(255)),
Column("isMaimai", Boolean),
Column("commonId", Integer),
Column("charaIllustId", Integer),
Column("romVersion", String(255)),
Column("judgeHeaven", Integer),
Column("regionId", Integer),
Column("machineType", Integer),
Column("ticketId", Integer),
mysql_charset="utf8mb4"
)
class ChuniRomVersion():
"""
Class used to easily compare rom version strings and map back to the internal integer version.
Used with methods that touch the playlog table.
"""
Versions = {}
def init_versions(cfg: ChuniConfig):
if len(ChuniRomVersion.Versions) > 0:
# dont bother with reinit
return
# Build up a easily comparible list of versions. Used when deriving romVersion from the playlog
all_versions = {
10: ChuniRomVersion("1.50.0"),
9: ChuniRomVersion("1.45.0"),
8: ChuniRomVersion("1.40.0"),
7: ChuniRomVersion("1.35.0"),
6: ChuniRomVersion("1.30.0"),
5: ChuniRomVersion("1.25.0"),
4: ChuniRomVersion("1.20.0"),
3: ChuniRomVersion("1.15.0"),
2: ChuniRomVersion("1.10.0"),
1: ChuniRomVersion("1.05.0"),
0: ChuniRomVersion("1.00.0")
}
# add the versions from the config
for ver in range(11,999):
cfg_ver = cfg.version.version(ver)
if cfg_ver:
all_versions[ver] = ChuniRomVersion(cfg_ver["rom"])
else:
break
# sort it by version number for easy iteration
ChuniRomVersion.Versions = dict(sorted(all_versions.items()))
def __init__(self, rom_version: str) -> None:
(major, minor, maint) = rom_version.split('.')
self.major = int(major)
self.minor = int(minor)
self.maint = int(maint)
self.version = rom_version
def __str__(self) -> str:
return self.version
def __eq__(self, other) -> bool:
return (self.major == other.major and
self.minor == other.minor and
self.maint == other.maint)
def __lt__(self, other) -> bool:
return (self.major < other.major) or \
(self.major == other.major and self.minor < other.minor) or \
(self.major == other.major and self.minor == other.minor and self.maint < other.maint)
def __gt__(self, other) -> bool:
return (self.major > other.major) or \
(self.major == other.major and self.minor > other.minor) or \
(self.major == other.major and self.minor == other.minor and self.maint > other.maint)
def get_int_version(self) -> int:
"""
Used when displaying the playlog to walk backwards from the recorded romVersion to our internal version number.
This is effectively a workaround to avoid recording our internal version number along with the romVersion in the db at insert time.
"""
for ver,rom in ChuniRomVersion.Versions.items():
# if the version matches exactly, great!
if self == rom:
return ver
# If this isnt the last version, use the next as an upper bound
if ver + 1 < len(ChuniRomVersion.Versions):
if self > rom and self < ChuniRomVersion.Versions[ver + 1]:
# this version fits in the middle! It must be a revision of the version
# e.g. 2.15.00 vs 2.16.00
return ver
else:
# this is the last version in the list.
# If its greate than this one and still the same major, this call it a match
if self.major == rom.major and self > rom:
return ver
# Only way we get here is if it was a version that started with "0." which is def invalid
return -1
class ChuniScoreData(BaseData):
async def get_courses(self, aime_id: int) -> Optional[Row]:
sql = select(course).where(course.c.user == aime_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = aime_id
course_data = self.fix_bools(course_data)
sql = insert(course).values(**course_data)
conflict = sql.on_duplicate_key_update(**course_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_scores(self, aime_id: int) -> Optional[Row]:
sql = select(best_score).where(best_score.c.user == aime_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
score_data["user"] = aime_id
score_data = self.fix_bools(score_data)
sql = insert(best_score).values(**score_data)
conflict = sql.on_duplicate_key_update(**score_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_playlogs(self, aime_id: int) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def get_playlog_rom_versions_by_int_version(self, version: int, aime_id: int = -1) -> Optional[str]:
# Get a set of all romVersion values present
sql = select([playlog.c.romVersion])
if aime_id != -1:
# limit results to a specific user
sql = sql.where(playlog.c.user == aime_id)
sql = sql.distinct()
result = await self.execute(sql)
if result is None:
return None
record_versions = result.fetchall()
# for each romVersion recorded, check if it maps back the current version we are operating on
matching_rom_versions = []
for v in record_versions:
if ChuniRomVersion(v[0]).get_int_version() == version:
matching_rom_versions += [v[0]]
self.logger.debug(f"romVersions {matching_rom_versions} map to version {version}")
return matching_rom_versions
async def get_playlogs_limited(self, aime_id: int, version: int, index: int, count: int) -> Optional[Row]:
# Get a list of all the recorded romVersions in the playlog
# for this user that map to the given version.
rom_versions = await self.get_playlog_rom_versions_by_int_version(version, aime_id)
if rom_versions is None:
return None
# Query results that have the matching romVersions
sql = select(playlog).where((playlog.c.user == aime_id) & (playlog.c.romVersion.in_(rom_versions))).order_by(playlog.c.id.desc()).limit(count).offset(index * count)
result = await self.execute(sql)
if result is None:
self.logger.info(f" aime_id {aime_id} has no playlog for version {version}")
return None
return result.fetchall()
async def get_user_playlogs_count(self, aime_id: int, version: int) -> Optional[Row]:
# Get a list of all the recorded romVersions in the playlog
# for this user that map to the given version.
rom_versions = await self.get_playlog_rom_versions_by_int_version(version, aime_id)
if rom_versions is None:
return None
# Query results that have the matching romVersions
sql = select(func.count()).where((playlog.c.user == aime_id) & (playlog.c.romVersion.in_(rom_versions)))
result = await self.execute(sql)
if result is None:
self.logger.info(f" aime_id {aime_id} has no playlog for version {version}")
return 0
return result.scalar()
async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
playlog_data["user"] = aime_id
playlog_data = self.fix_bools(playlog_data)
# If the romVersion is not in the data (Version 10 and earlier), look it up from our internal mapping
if "romVersion" not in playlog_data:
playlog_data["romVersion"] = ChuniRomVersion.Versions[version]
sql = insert(playlog).values(**playlog_data)
result = await self.execute(sql)
if result is None:
return None
return result.lastrowid
async def get_rankings(self, version: int) -> Optional[List[Dict]]:
# Get a list of all the recorded romVersions in the playlog for the given version
rom_versions = await self.get_playlog_rom_versions_by_int_version(version)
if rom_versions is None:
return None
# Query results that have the matching romVersions
sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.in_(rom_versions))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
result = await self.execute(sql)
if result is None:
return None
rows = result.fetchall()
return [dict(row) for row in rows]
async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
sql = select(best_score).where(best_score.c.user == rival_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()