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):
+