[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>
This commit is contained in:
parent
1d8e31d4ab
commit
b04840f3dd
@ -75,9 +75,14 @@ class ChuniVersionConfig:
|
||||
in the form of:
|
||||
11: {"rom": 2.00.00, "data": 2.00.00}
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
versions = CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "version", default={}
|
||||
)[version]
|
||||
)
|
||||
|
||||
if version not in versions.keys():
|
||||
return None
|
||||
|
||||
return versions[version]
|
||||
|
||||
|
||||
class ChuniCryptoConfig:
|
||||
|
@ -1,13 +1,17 @@
|
||||
from core.data import Data
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.schema import *
|
||||
|
||||
from .config import ChuniConfig
|
||||
|
||||
class ChuniData(Data):
|
||||
def __init__(self, cfg: CoreConfig) -> None:
|
||||
def __init__(self, cfg: CoreConfig, chuni_cfg: ChuniConfig = None) -> None:
|
||||
super().__init__(cfg)
|
||||
|
||||
self.item = ChuniItemData(cfg, self.session)
|
||||
self.profile = ChuniProfileData(cfg, self.session)
|
||||
self.score = ChuniScoreData(cfg, self.session)
|
||||
self.static = ChuniStaticData(cfg, self.session)
|
||||
|
||||
# init rom versioning for use with score playlog data
|
||||
if chuni_cfg:
|
||||
ChuniRomVersion.init_versions(chuni_cfg)
|
||||
|
@ -2,6 +2,7 @@ from typing import List
|
||||
from starlette.routing import Route, Mount
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response, RedirectResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from os import path
|
||||
import yaml
|
||||
import jinja2
|
||||
@ -81,12 +82,12 @@ class ChuniFrontend(FE_Base):
|
||||
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||
) -> None:
|
||||
super().__init__(cfg, environment)
|
||||
self.data = ChuniData(cfg)
|
||||
self.game_cfg = ChuniConfig()
|
||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||
)
|
||||
self.data = ChuniData(cfg, self.game_cfg)
|
||||
self.nav_name = "Chunithm"
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
@ -97,8 +98,12 @@ class ChuniFrontend(FE_Base):
|
||||
Route("/", self.render_GET_playlog, methods=['GET']),
|
||||
Route("/{index}", self.render_GET_playlog, methods=['GET']),
|
||||
]),
|
||||
Route("/favorites", self.render_GET_favorites, methods=['GET']),
|
||||
Route("/update.name", self.update_name, methods=['POST']),
|
||||
Route("/update.favorite_music_playlog", self.update_favorite_music_playlog, methods=['POST']),
|
||||
Route("/update.favorite_music_favorites", self.update_favorite_music_favorites, methods=['POST']),
|
||||
Route("/version.change", self.version_change, methods=['POST']),
|
||||
Mount('/img', app=StaticFiles(directory='titles/chuni/img'), name="img")
|
||||
]
|
||||
|
||||
async def render_GET(self, request: Request) -> bytes:
|
||||
@ -205,7 +210,8 @@ class ChuniFrontend(FE_Base):
|
||||
else:
|
||||
index = int(path_index) - 1 # 0 and 1 are 1st page
|
||||
user_id = usr_sesh.user_id
|
||||
playlog_count = await self.data.score.get_user_playlogs_count(user_id)
|
||||
version = usr_sesh.chunithm_version
|
||||
playlog_count = await self.data.score.get_user_playlogs_count(user_id, version)
|
||||
if playlog_count < index * 20 :
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
@ -213,31 +219,107 @@ class ChuniFrontend(FE_Base):
|
||||
sesh=vars(usr_sesh),
|
||||
playlog_count=0
|
||||
), media_type="text/html; charset=utf-8")
|
||||
playlog = await self.data.score.get_playlogs_limited(user_id, index, 20)
|
||||
playlog = await self.data.score.get_playlogs_limited(user_id, version, index, 20)
|
||||
playlog_with_title = []
|
||||
for record in playlog:
|
||||
music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, record.musicId, record.level)
|
||||
for idx,record in enumerate(playlog):
|
||||
music_chart = await self.data.static.get_music_chart(version, record.musicId, record.level)
|
||||
if music_chart:
|
||||
difficultyNum=music_chart.level
|
||||
artist=music_chart.artist
|
||||
title=music_chart.title
|
||||
(jacket, ext) = path.splitext(music_chart.jacketPath)
|
||||
jacket += ".png"
|
||||
else:
|
||||
difficultyNum=0
|
||||
artist="unknown"
|
||||
title="musicid: " + str(record.musicId)
|
||||
jacket = "unknown.png"
|
||||
|
||||
# Check if this song is a favorite so we can populate the add/remove button
|
||||
is_favorite = await self.data.item.is_favorite(user_id, version, record.musicId)
|
||||
|
||||
playlog_with_title.append({
|
||||
# Values for the actual readable results
|
||||
"raw": record,
|
||||
"title": title,
|
||||
"difficultyNum": difficultyNum,
|
||||
"artist": artist,
|
||||
"jacket": jacket,
|
||||
# Values used solely for favorite updates
|
||||
"idx": idx,
|
||||
"musicId": record.musicId,
|
||||
"isFav": is_favorite
|
||||
})
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
user_id=usr_sesh.user_id,
|
||||
user_id=user_id,
|
||||
playlog=playlog_with_title,
|
||||
playlog_count=playlog_count
|
||||
playlog_count=playlog_count,
|
||||
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
||||
), media_type="text/html; charset=utf-8")
|
||||
else:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
async def render_GET_favorites(self, request: Request) -> bytes:
|
||||
template = self.environment.get_template(
|
||||
"titles/chuni/templates/chuni_favorites.jinja"
|
||||
)
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
usr_sesh = UserSession()
|
||||
|
||||
if usr_sesh.user_id > 0:
|
||||
if usr_sesh.chunithm_version < 0:
|
||||
return RedirectResponse("/game/chuni/", 303)
|
||||
|
||||
user_id = usr_sesh.user_id
|
||||
version = usr_sesh.chunithm_version
|
||||
favorites = await self.data.item.get_all_favorites(user_id, version, 1)
|
||||
favorites_count = len(favorites)
|
||||
favorites_with_title = []
|
||||
favorites_by_genre = dict()
|
||||
for idx,favorite in enumerate(favorites):
|
||||
song = await self.data.static.get_song(favorite.favId)
|
||||
if song:
|
||||
# we likely got multiple results - one for each chart. Just use the first
|
||||
artist=song.artist
|
||||
title=song.title
|
||||
genre=song.genre
|
||||
(jacket, ext) = path.splitext(song.jacketPath)
|
||||
jacket += ".png"
|
||||
else:
|
||||
artist="unknown"
|
||||
title="musicid: " + str(favorite.favId)
|
||||
genre="unknown"
|
||||
jacket = "unknown.png"
|
||||
|
||||
# add a new collection for the genre if this is our first time seeing it
|
||||
if genre not in favorites_by_genre:
|
||||
favorites_by_genre[genre] = []
|
||||
|
||||
# add the song to the appropriate genre collection
|
||||
favorites_by_genre[genre].append({
|
||||
"idx": idx,
|
||||
"title": title,
|
||||
"artist": artist,
|
||||
"jacket": jacket,
|
||||
"favId": favorite.favId
|
||||
})
|
||||
|
||||
# Sort favorites by title before rendering the page
|
||||
for g in favorites_by_genre:
|
||||
favorites_by_genre[g].sort(key=lambda x: x["title"].lower())
|
||||
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
user_id=user_id,
|
||||
favorites_by_genre=favorites_by_genre,
|
||||
favorites_count=favorites_count,
|
||||
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
||||
), media_type="text/html; charset=utf-8")
|
||||
else:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
@ -279,6 +361,32 @@ class ChuniFrontend(FE_Base):
|
||||
|
||||
return RedirectResponse("/game/chuni/?s=1", 303)
|
||||
|
||||
async def update_favorite_music(self, request: Request, retPage: str):
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
return RedirectResponse(retPage, 303)
|
||||
|
||||
user_id = usr_sesh.user_id
|
||||
version = usr_sesh.chunithm_version
|
||||
form_data = await request.form()
|
||||
music_id: str = form_data.get("musicId")
|
||||
isAdd: int = int(form_data.get("isAdd"))
|
||||
|
||||
if isAdd:
|
||||
if await self.data.item.put_favorite_music(user_id, version, music_id) == None:
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
else:
|
||||
if await self.data.item.delete_favorite_music(user_id, version, music_id) == None:
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse(retPage, 303)
|
||||
|
||||
async def update_favorite_music_playlog(self, request: Request):
|
||||
return await self.update_favorite_music(request, "/game/chuni/playlog")
|
||||
|
||||
async def update_favorite_music_favorites(self, request: Request):
|
||||
return await self.update_favorite_music(request, "/game/chuni/favorites")
|
||||
|
||||
async def version_change(self, request: Request):
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
|
BIN
titles/chuni/img/jacket/unknown.png
Normal file
BIN
titles/chuni/img/jacket/unknown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -2,6 +2,7 @@ from typing import Optional
|
||||
from os import walk, path
|
||||
import xml.etree.ElementTree as ET
|
||||
from read import BaseReader
|
||||
from PIL import Image
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.database import ChuniData
|
||||
@ -164,6 +165,16 @@ class ChuniReader(BaseReader):
|
||||
|
||||
for jaketFile in xml_root.findall("jaketFile"): # nice typo, SEGA
|
||||
jacket_path = jaketFile.find("path").text
|
||||
# Convert the image to png and save it for use in the frontend
|
||||
jacket_filename_src = f"{root}/{dir}/{jacket_path}"
|
||||
(pre, ext) = path.splitext(jacket_path)
|
||||
jacket_filename_dst = f"titles/chuni/img/jacket/{pre}.png"
|
||||
if path.exists(jacket_filename_src) and not path.exists(jacket_filename_dst):
|
||||
try:
|
||||
im = Image.open(jacket_filename_src)
|
||||
im.save(jacket_filename_dst)
|
||||
except Exception:
|
||||
self.logger.warning(f"Failed to convert {jacket_path} to png")
|
||||
|
||||
for fumens in xml_root.findall("fumens"):
|
||||
for MusicFumenData in fumens.findall("MusicFumenData"):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from titles.chuni.schema.profile import ChuniProfileData
|
||||
from titles.chuni.schema.score import ChuniScoreData
|
||||
from titles.chuni.schema.score import ChuniScoreData, ChuniRomVersion
|
||||
from titles.chuni.schema.item import ChuniItemData
|
||||
from titles.chuni.schema.static import ChuniStaticData
|
||||
|
||||
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]
|
||||
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniRomVersion", "ChuniItemData", "ChuniStaticData"]
|
||||
|
@ -359,6 +359,25 @@ class ChuniItemData(BaseData):
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def is_favorite(
|
||||
self, user_id: int, version: int, fav_id: int, fav_kind: int = 1
|
||||
) -> bool:
|
||||
|
||||
sql = favorite.select(
|
||||
and_(
|
||||
favorite.c.version == version,
|
||||
favorite.c.user == user_id,
|
||||
favorite.c.favId == fav_id,
|
||||
favorite.c.favKind == fav_kind,
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
return True if len(result.all()) else False
|
||||
|
||||
async def get_all_favorites(
|
||||
self, user_id: int, version: int, fav_kind: int = 1
|
||||
) -> Optional[List[Row]]:
|
||||
@ -421,6 +440,31 @@ class ChuniItemData(BaseData):
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_favorite_music(self, user_id: int, version: int, music_id: int) -> Optional[int]:
|
||||
sql = insert(favorite).values(user=user_id, version=version, favId=music_id, favKind=1)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(user=user_id, version=version, favId=music_id, favKind=1)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def delete_favorite_music(self, user_id: int, version: int, music_id: int) -> Optional[int]:
|
||||
sql = delete(favorite).where(
|
||||
and_(
|
||||
favorite.c.user==user_id,
|
||||
favorite.c.version==version,
|
||||
favorite.c.favId==music_id,
|
||||
favorite.c.favKind==1
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
||||
character_data["user"] = user_id
|
||||
|
||||
|
@ -8,6 +8,7 @@ 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",
|
||||
@ -140,6 +141,92 @@ playlog = Table(
|
||||
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]:
|
||||
@ -190,45 +277,66 @@ class ChuniScoreData(BaseData):
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_playlogs_limited(self, aime_id: int, index: int, count: int) -> Optional[Row]:
|
||||
sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.id.desc()).limit(count).offset(index * count)
|
||||
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:
|
||||
self.logger.warning(f" aime_id {aime_id} has no playlog ")
|
||||
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) -> Optional[Row]:
|
||||
sql = select(func.count()).where(playlog.c.user == aime_id)
|
||||
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.warning(f" aime_id {aime_id} has no playlog ")
|
||||
return 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]:
|
||||
# Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted
|
||||
# We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert
|
||||
# This matters both for gameRankings, as well as a future DB update to keep version data separate
|
||||
romVer = {
|
||||
10: "1.50.0",
|
||||
9: "1.45.0",
|
||||
8: "1.40.0",
|
||||
7: "1.35.0",
|
||||
6: "1.30.0",
|
||||
5: "1.25.0",
|
||||
4: "1.20.0",
|
||||
3: "1.15.0",
|
||||
2: "1.10.0",
|
||||
1: "1.05.0",
|
||||
0: "1.00.0"
|
||||
}
|
||||
|
||||
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"] = romVer.get(version, "1.00.0")
|
||||
playlog_data["romVersion"] = ChuniRomVersion.Versions[version]
|
||||
|
||||
sql = insert(playlog).values(**playlog_data)
|
||||
|
||||
@ -238,27 +346,13 @@ class ChuniScoreData(BaseData):
|
||||
return result.lastrowid
|
||||
|
||||
async def get_rankings(self, version: int) -> Optional[List[Dict]]:
|
||||
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
|
||||
# This prevents tracks that are not accessible in your version from counting towards the 10 results
|
||||
romVer = {
|
||||
15: "2.20%",
|
||||
14: "2.15%",
|
||||
13: "2.10%",
|
||||
12: "2.05%",
|
||||
11: "2.00%",
|
||||
10: "1.50%",
|
||||
9: "1.45%",
|
||||
8: "1.40%",
|
||||
7: "1.35%",
|
||||
6: "1.30%",
|
||||
5: "1.25%",
|
||||
4: "1.20%",
|
||||
3: "1.15%",
|
||||
2: "1.10%",
|
||||
1: "1.05%",
|
||||
0: "1.00%"
|
||||
}
|
||||
sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
|
||||
# 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:
|
||||
|
@ -454,7 +454,7 @@ class ChuniStaticData(BaseData):
|
||||
return result.fetchone()
|
||||
|
||||
async def get_song(self, music_id: int) -> Optional[Row]:
|
||||
sql = music.select(music.c.id == music_id)
|
||||
sql = music.select(music.c.songId == music_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
|
55
titles/chuni/templates/chuni_favorites.jinja
Normal file
55
titles/chuni/templates/chuni_favorites.jinja
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% include 'titles/chuni/templates/css/chuni_style.css' %}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||
{% if favorites_by_genre is defined and favorites_by_genre is not none %}
|
||||
<div class="row">
|
||||
<h1 style="text-align: center;">{{ cur_version_name }}</h1>
|
||||
<h4 style="text-align: center;">Favorite Count: {{ favorites_count }}</h4>
|
||||
{% for key, genre in favorites_by_genre.items() %}
|
||||
<h2 style="text-align: center; padding-top: 32px">{{ key }}</h2>
|
||||
{% for favorite in genre %}
|
||||
<form id="fav_{{ favorite.idx }}" action="/game/chuni/update.favorite_music_favorites" method="post" style="display: none;">
|
||||
<input class="form-control" form="fav_{{ favorite.idx }}" id="musicId" name="musicId" type="hidden" value="{{ favorite.favId }}">
|
||||
<input class="form-control" form="fav_{{ favorite.idx }}" id="isAdd" name="isAdd" type="hidden" value="0">
|
||||
</form>
|
||||
<div class="col-lg-6 mt-3">
|
||||
<div class="card bg-card rounded card-hover">
|
||||
<div class="card-body row">
|
||||
<div class="col-3" style="text-align: center;">
|
||||
<img src="img/jacket/{{ favorite.jacket }}" width="100%">
|
||||
</div>
|
||||
<div class="col scrolling-text">
|
||||
<h5 class="card-text"> {{ favorite.title }} </h5>
|
||||
<br>
|
||||
<h6 class="card-text"> {{ favorite.artist }} </h6>
|
||||
<br><br>
|
||||
<div style="text-align: right;">
|
||||
<input type=submit class="btn btn-secondary btn-fav-remove" type="button" form="fav_{{ favorite.idx }}" value="Remove">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
|
||||
var parentWidth = $(this).parent().width();
|
||||
var elementWidth = $(this).outerWidth();
|
||||
var elementWidthWithPadding = $(this).outerWidth(true);
|
||||
|
||||
if (elementWidthWithPadding > parentWidth) {
|
||||
$(this).addClass('scrolling');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
@ -4,6 +4,7 @@
|
||||
<li><a class="nav-link" href="/game/chuni">PROFILE</a></li>
|
||||
<li><a class="nav-link" href="/game/chuni/rating">RATING</a></li>
|
||||
<li><a class="nav-link" href="/game/chuni/playlog">RECORD</a></li>
|
||||
<li><a class="nav-link" href="/game/chuni/favorites">FAVORITES</a></li>
|
||||
<li><a class="nav-link" href="/game/chuni/musics">MUSICS</a></li>
|
||||
<li><a class="nav-link" href="/game/chuni/userbox">USER BOX</a></li>
|
||||
</ul>
|
||||
@ -17,6 +18,8 @@
|
||||
$('.nav-link[href="/game/chuni/playlog"]').addClass('active');
|
||||
} else if (currentPath.startsWith('/game/chuni/rating')) {
|
||||
$('.nav-link[href="/game/chuni/rating"]').addClass('active');
|
||||
} else if (currentPath.startsWith('/game/chuni/favorites')) {
|
||||
$('.nav-link[href="/game/chuni/favorites"]').addClass('active');
|
||||
} else if (currentPath.startsWith('/game/chuni/musics')) {
|
||||
$('.nav-link[href="/game/chuni/musics"]').addClass('active');
|
||||
}
|
||||
|
@ -7,25 +7,36 @@
|
||||
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||
{% if playlog is defined and playlog is not none %}
|
||||
<div class="row">
|
||||
<h4 style="text-align: center;">Playlog counts: {{ playlog_count }}</h4>
|
||||
<h1 style="text-align: center;">{{ cur_version_name }}</h1>
|
||||
<h4 style="text-align: center;">Playlog Count: {{ playlog_count }}</h4>
|
||||
{% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
|
||||
{% set difficultyName = ['normal', 'hard', 'expert', 'master', 'ultimate'] %}
|
||||
{% for record in playlog %}
|
||||
<form id="fav_{{ record.idx }}" action="/game/chuni/update.favorite_music_playlog" method="post" style="display: none;">
|
||||
<input class="form-control" form="fav_{{ record.idx }}" id="musicId" name="musicId" type="hidden" value="{{ record.musicId }}">
|
||||
<input class="form-control" form="fav_{{ record.idx }}" id="isAdd" name="isAdd" type="hidden" value="{{ 0 if record.isFav else 1 }}">
|
||||
</form>
|
||||
<div class="col-lg-6 mt-3">
|
||||
<div class="card bg-card rounded card-hover">
|
||||
<div class="card-header row">
|
||||
<div class="col-8 scrolling-text">
|
||||
<div class="col-auto fav" title="{{ ('Remove' if record.isFav else 'Add') + ' Favorite'}}">
|
||||
<h1><input type=submit class="fav {{ 'fav-set' if record.isFav else '' }}" type="button" form="fav_{{ record.idx }}" value="{{ '★' if record.isFav else '☆' }} "></h1>
|
||||
</div>
|
||||
<div class="col scrolling-text">
|
||||
<h5 class="card-text"> {{ record.title }} </h5>
|
||||
<br>
|
||||
<h6 class="card-text"> {{ record.artist }} </h6>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-auto">
|
||||
<h6 class="card-text">{{ record.raw.userPlayDate }}</h6>
|
||||
<h6 class="card-text">TRACK {{ record.raw.track }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body row">
|
||||
<div class="col-3" style="text-align: center;">
|
||||
<div class="col-sm" style="text-align: center;">
|
||||
<img src="../img/jacket/{{ record.jacket }}" width="100%">
|
||||
</div>
|
||||
<div class="col" style="text-align: center;">
|
||||
<h4 class="card-text">{{ record.raw.score }}</h4>
|
||||
<h2>{{ rankName[record.raw.rank] }}</h2>
|
||||
<h6
|
||||
@ -33,10 +44,10 @@
|
||||
{{ difficultyName[record.raw.level] }}  {{ record.difficultyNum }}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: center;">
|
||||
<div class="col-4" style="text-align: center;">
|
||||
<table class="table-small table-rowdistinc">
|
||||
<tr>
|
||||
<td>JUSTICE CRITIAL</td>
|
||||
<td>JUSTICE CRITICAL</td>
|
||||
<td>
|
||||
{{ record.raw.judgeCritical + record.raw.judgeHeaven }}
|
||||
</td>
|
||||
|
@ -192,4 +192,21 @@ caption {
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.fav {
|
||||
padding: 0;
|
||||
padding-left: 4px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fav-set {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.btn-fav-remove {
|
||||
padding:10px;
|
||||
width:100%;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user