diff --git a/api/database.py b/api/database.py index 441401f..55b6d9d 100644 --- a/api/database.py +++ b/api/database.py @@ -1,5 +1,5 @@ import sqlalchemy -from sqlalchemy import Table, Column, Integer, String, DateTime, Boolean +from sqlalchemy import Table, Column, Integer, String, DateTime, Boolean, JSON from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy import select, update @@ -19,6 +19,13 @@ DB_NAME = "player.db" DB_PATH = os.path.join(os.getcwd(), DB_NAME) DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}" +CACHE_DB_NAME = "cache.db" +CACHE_DB_PATH = os.path.join(os.getcwd(), CACHE_DB_NAME) +CACHE_DATABASE_URL = f"sqlite+aiosqlite:///{CACHE_DB_PATH}" + +cache_database = databases.Database(CACHE_DATABASE_URL) +cache_metadata = sqlalchemy.MetaData() + database = databases.Database(DATABASE_URL) metadata = sqlalchemy.MetaData() @@ -105,10 +112,27 @@ admins = Table( Column("token", String(256), unique=True, nullable=True) ) +ranking_cache = Table( + "ranking_cache", + cache_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("key", String(16), nullable=False), + Column("value", JSON, nullable=False), + Column("expire_at", Integer) +) + async def init_db(): global redis if not os.path.exists(DB_PATH): print("[DB] Creating new database:", DB_PATH) + + if not os.path.exists(CACHE_DB_PATH): + print("[DB] Creating new cache database:", CACHE_DB_PATH) + + cache_engine = create_async_engine(CACHE_DATABASE_URL, echo=False) + async with cache_engine.begin() as conn: + await conn.run_sync(cache_metadata.create_all) + await cache_engine.dispose() engine = create_async_engine(DATABASE_URL, echo=False) diff --git a/api/play.py b/api/play.py index 253e51c..bb19797 100644 --- a/api/play.py +++ b/api/play.py @@ -8,7 +8,7 @@ import xml.etree.ElementTree as ET from config import ROOT_FOLDER, START_COIN, COIN_REWARD, AUTHORIZATION_NEEDED -from api.database import database, user, daily_reward, result, check_blacklist, check_whitelist +from api.database import database, cache_database, ranking_cache, user, daily_reward, result, check_blacklist, check_whitelist from api.crypt import decrypt_fields from api.templates import START_STAGES, EXP_UNLOCKED_SONGS @@ -47,6 +47,15 @@ async def result_request(request: Request): ver = decrypted_fields[b'ver'][0].decode() mike = decrypted_fields[b'mike'][0].decode() + cache_key = f"{track_id}-{mode}" + + # delete cache with key first + + delete_query = ranking_cache.delete().where(ranking_cache.c.key == cache_key) + await cache_database.execute(delete_query) + + # add coins, skip 4max placeholder songs + if int(track_id) not in range(616, 1024) or int(mode) not in range(0, 4): query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id) row = await database.fetch_one(query) diff --git a/api/ranking.py b/api/ranking.py index 028a63d..4157bc8 100644 --- a/api/ranking.py +++ b/api/ranking.py @@ -3,11 +3,12 @@ from starlette.requests import Request from starlette.routing import Route import os import json +import time from sqlalchemy import select, update from config import AUTHORIZATION_NEEDED, USE_REDIS_CACHE -from api.database import database, check_whitelist, check_blacklist, redis, result, daily_reward, user +from api.database import database, cache_database, ranking_cache, check_whitelist, check_blacklist, redis, result, daily_reward, user from api.crypt import decrypt_fields, encryptAES from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST from api.misc import inform_page, safe_int @@ -109,11 +110,24 @@ async def ranking_detail(request: Request): play_results = None user_result = None device_result = None + cache_key = f"{song_id}-{mode}" if USE_REDIS_CACHE: - cache_key = f"{song_id}-{mode}" cached = await redis.get(cache_key) else: - cached = False + cache_db_query = ranking_cache.select().where(ranking_cache.c.key == cache_key) + cache_db_result = await cache_database.fetch_one(cache_db_query) + if cache_db_result: + timestamp = time.time() + if cache_db_result["expire_at"] and cache_db_result["expire_at"] < timestamp and song_id == -1: + # Global LB, Cache expired, delete it + delete_query = ranking_cache.delete().where(ranking_cache.c.key == cache_key) + await cache_database.execute(delete_query) + cached = False + else: + # individual LB result invalidated upon score submission, no need to check expire time + cached = cache_db_result["value"] + else: + cached = False if (song_id == -1): # Filter out the mobile/AC modes @@ -124,9 +138,10 @@ async def ranking_detail(request: Request): else: exclude = [1, 2, 3] - if cached and USE_REDIS_CACHE: sorted_players = json.loads(cached) + elif cached: + sorted_players = cached else: query = select(result.c.vid, result.c.sid, result.c.mode, result.c.avatar, result.c.score) @@ -176,6 +191,15 @@ async def ranking_detail(request: Request): if USE_REDIS_CACHE: await redis.set(cache_key, json.dumps(sorted_players), ex=300) + # log to cache db + query = ranking_cache.insert().values( + key=cache_key, + value=sorted_players, + expire_at=int(time.time()) + 180 # 3 minutes expiration + ) + await cache_database.execute(query) + + username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})" player_rank = None @@ -229,19 +253,32 @@ async def ranking_detail(request: Request): else: if cached and USE_REDIS_CACHE: play_results = json.loads(cached) + + elif cached: + play_results = cached else: - query = select(result).where((result.c.id == song_id) & (result.c.mode == mode)) + query = select(result).where((result.c.id == song_id) & (result.c.mode == mode)).order_by(result.c.score.desc()) play_results = await database.fetch_all(query) - play_results = sorted(play_results, key=lambda x: int(x[8]), reverse=True) + play_results = [dict(row) for row in play_results] + if USE_REDIS_CACHE: await redis.set(cache_key, json.dumps(play_results), ex=300) + # log to cache db + query = ranking_cache.insert().values( + key=cache_key, + value=play_results, + expire_at=None # individual LB, no expiration needed + ) + await cache_database.execute(query) + query = select(user).where(user.c.device_id == device_id) user_result = await database.fetch_one(query) query = select(daily_reward).where(daily_reward.c.device_id == device_id) device_result = await database.fetch_one(query) + device_result = dict(device_result) if device_result else {"title": "1", "avatar": 1} user_id = user_result[0] if user_result else None username = user_result[1] if user_result else f"Guest({device_id[-6:]})" @@ -254,16 +291,16 @@ async def ranking_detail(request: Request): ) if not play_record: - play_record = next((record for record in play_results if record[1] == device_id and record[3] is None), None) + play_record = next((record for record in play_results if record['vid'] == device_id and record['sid'] is None), None) player_rank = None - avatar_index = str(play_record[7]) if play_record else "1" - user_score = play_record[8] if play_record else 0 + avatar_index = str(play_record['avatar']) if play_record else "1" + user_score = play_record['score'] if play_record else 0 for rank, result_obj in enumerate(play_results, start=1): - if user_result and safe_int(result_obj[3]) == user_id: + if user_result and safe_int(result_obj['sid']) == user_id: player_rank = rank break - elif result_obj[1] == device_id and result_obj[3] in (None, ''): + elif result_obj['vid'] == device_id and result_obj['sid'] in (None, ''): player_rank = rank break @@ -273,7 +310,7 @@ async def ranking_detail(request: Request): Player Avatar
{username}
- Player Title + Player Title
{user_score}
@@ -284,25 +321,25 @@ async def ranking_detail(request: Request): """ for rank, record in enumerate(play_results, start=1): - username = f"Guest({record[1][-6:]})" + username = f"Guest({record['vid'][-6:]})" device_info = None - if record[3]: - query = select(user.c.username).where(user.c.id == record[3]) + if record['sid']: + query = select(user.c.username).where(user.c.id == record['sid']) user_data = await database.fetch_one(query) if user_data: username = user_data["username"] - query = select(daily_reward.c.title).where(daily_reward.c.device_id == record[1]) + query = select(daily_reward.c.title).where(daily_reward.c.device_id == record['vid']) device_title = await database.fetch_one(query) if device_title: device_info = device_title["title"] else: device_info = "1" - avatar_id = record[7] if record[7] else 1 + avatar_id = record['avatar'] if record['avatar'] else 1 avatar_url = f"/files/image/icon/avatar/{avatar_id}.png" - score = record[8] + score = record['score'] html += f"""