from starlette.responses import HTMLResponse 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, 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 async def ranking(request: Request): decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: return HTMLResponse("""

Invalid request data

""", status_code=400) should_serve = True if AUTHORIZATION_NEEDED: should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() html = "" file_path = os.path.join("files", "ranking.html") try: with open(file_path, "r", encoding="utf-8") as file: html_content = file.read().format(text=html) except FileNotFoundError: return HTMLResponse("""

Ranking file not found

""", status_code=500) return HTMLResponse(html_content) else: return HTMLResponse("""

Access denied

""", status_code=403) async def ranking_detail(request: Request): decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: return HTMLResponse("""

Invalid request data

""", status_code=400) should_serve = True if AUTHORIZATION_NEEDED: should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() song_id = int(decrypted_fields[b'song_id'][0].decode()) mode = int(decrypted_fields[b'mode'][0].decode()) button_labels = [] difficulty_levels = [] song_name = "" if (song_id == -1): song_name = "Total Score" difficulty_levels = [] button_labels = ["All", "Mobile", "Arcade"] else: song_name = SONG_LIST[song_id]["name_en"] difficulty_levels = SONG_LIST[song_id]["difficulty_levels"] button_labels = ["Easy", "Normal", "Hard"] html = f"""
{song_name}
""" button_modes = [1, 2, 3] if (len(difficulty_levels) == 6): button_labels.extend(["AC-Easy", "AC-Normal", "AC-Hard"]) button_modes.extend([11, 12, 13]) if song_id > 615: button_modes = [x for x in button_modes if x not in [1, 2, 3]] button_labels = [x for x in button_labels if x not in ["Easy", "Normal", "Hard"]] row_start = '
' row_end = '
' row_content = [] for i, (label, mode_value) in enumerate(zip(button_labels, button_modes)): if mode_value == mode: row_content.append(f"""
{label}
""") else: encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=" + str(song_id) + "&mode=" + str(mode_value) + "&dummy=").encode("utf-8")) row_content.append(f"""{label}""") if len(row_content) == 3: html += row_start + ''.join(row_content) + row_end row_content = [] # Reset row content play_results = None user_result = None device_result = None cache_key = f"{song_id}-{mode}" if USE_REDIS_CACHE: cached = await redis.get(cache_key) else: 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 if (mode == 1): exclude = [] elif mode == 2: exclude = [11, 12, 13] 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) play_results = await database.fetch_all(query) query = select(daily_reward.c.device_id, daily_reward.c.title, daily_reward.c.avatar) device_results_raw = await database.fetch_all(query) device_results = {row["device_id"]: {"title": row["title"], "avatar": row["avatar"]} for row in device_results_raw} query = select(user.c.id, user.c.username, user.c.device_id) user_results_raw = await database.fetch_all(query) user_results = {row["id"]: {"username": row["username"], "device_id": row["device_id"]} for row in user_results_raw} query = select(user).where(user.c.device_id == device_id) cur_user = await database.fetch_one(query) player_scores = {} filtered_play_results = [play for play in play_results if int(play[2]) not in exclude] for play in filtered_play_results: did = play[0] sid = play[1] avatar = play[3] score = play[4] username, title = None, None if sid: sid = int(sid) if sid in user_results: username = user_results[sid]["username"] did = user_results[sid]["device_id"] else: # Guest username = f"Guest({did[-6:]})" # title is device-specific title = device_results.get(did, {}).get("title", "1") if username in player_scores: player_scores[username]["score"] += int(score) player_scores[username]["avatar"] = avatar # But avatar is based on latest play submission player_scores[username]["title"] = title else: player_scores[username] = {"score": int(score), "avatar": avatar, "title": title} sorted_players = sorted(player_scores.items(), key=lambda x: x[1]["score"], reverse=True) 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 user_score = 0 avatar = "1" title = "1" for rank, (player_name, data) in enumerate(sorted_players, start=1): if player_name == username: player_rank = rank user_score = data["score"] avatar = data["avatar"] title = data["title"] break if player_rank is None: device_data = next((device for device in device_results if device[1] == device_id), None) if device_data: avatar = device_data["avatar"] title = device_data["title"] html += f"""
You
{"#" + str(player_rank) if player_rank else "N/A"}
Player Avatar
{username}
Player Title
{user_score}
""" html += """
""" # Loop leaderboard for rank, (username, data) in enumerate(sorted_players, start=1): html += f"""
#{rank}
Avatar
{username}
Title
{data['score']}
""" 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)).order_by(result.c.score.desc()) play_results = await database.fetch_all(query) 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:]})" play_record = None if user_id: play_record = next( (record for record in play_results if safe_int(record[3]) == user_id), None ) if not play_record: 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['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['sid']) == user_id: player_rank = rank break elif result_obj['vid'] == device_id and result_obj['sid'] in (None, ''): player_rank = rank break html += f"""
You
{"#" + str(player_rank) if player_rank else "N/A"}
Player Avatar
{username}
Player Title
{user_score}
""" html += """
""" for rank, record in enumerate(play_results, start=1): username = f"Guest({record['vid'][-6:]})" device_info = None 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['vid']) device_title = await database.fetch_one(query) if device_title: device_info = device_title["title"] else: device_info = "1" avatar_id = record['avatar'] if record['avatar'] else 1 avatar_url = f"/files/image/icon/avatar/{avatar_id}.png" score = record['score'] html += f"""
#{rank}
Avatar
{username}
Title
{score}
""" html += "
" encrypted_mass = encryptAES(("vid=" + device_id + "&dummy=").encode("utf-8")) html += f""" Go Back """ file_path = os.path.join("files", "ranking.html") try: with open(file_path, "r", encoding="utf-8") as file: html_content = file.read().format(text=html) except FileNotFoundError: return HTMLResponse("""

Ranking file not found

""", status_code=500) return HTMLResponse(html_content) else: return HTMLResponse("""

Access denied

""", status_code=403) async def status(request: Request): decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: return HTMLResponse("""

Invalid request data

""", status_code=400) should_serve = True if AUTHORIZATION_NEEDED: should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() set_title = int(decrypted_fields[b'set_title'][0].decode()) if b'set_title' in decrypted_fields else None page_id = int(decrypted_fields[b'page_id'][0].decode()) if b'page_id' in decrypted_fields else 0 if set_title: update_query = ( update(daily_reward) .where(daily_reward.c.device_id == device_id) .values(title=set_title) ) await database.execute(update_query) query = select(daily_reward).where(daily_reward.c.device_id == device_id) user_data = await database.fetch_one(query) user_name = f"Guest({device_id[-6:]})" if user_data: query = select(user.c.username).where(user.c.device_id == device_id) user_result = await database.fetch_one(query) if user_result: user_name = user_result["username"] html = "" if user_data: player_element = f"""
Player Avatar
{user_name}
Player Title
Level {user_data['lvl']}
""" html += player_element page_name = ["Special", "Normal", "Master", "God"] buttons_html = '
' for i, name in enumerate(page_name): if i == page_id: buttons_html += f"""
{name}
""" else: encrypted_mass = encryptAES(f"vid={device_id}&page_id={i}&dummy=".encode("utf-8")) buttons_html += f""" {name} """ buttons_html += '
' html += f"
{buttons_html}
" selected_titles = TITLE_LISTS.get(page_id, []) titles_html = '
' for index, num in enumerate(selected_titles): if index % 2 == 0: if index != 0: titles_html += '
' titles_html += '
' if num == user_data["title"]: titles_html += f""" Title {num} """ else: encrypted_mass = encryptAES(f"vid={device_id}&title_id={num}&page_id={page_id}&dummy=".encode("utf-8")) titles_html += f""" Title {num} """ titles_html += '
' html += titles_html file_path = os.path.join("files", "status.html") try: with open(file_path, "r", encoding="utf-8") as file: html_content = file.read().format(text=html) except FileNotFoundError: return HTMLResponse("""

Status file not found

""", status_code=500) return HTMLResponse(html_content) else: return HTMLResponse("""

Access denied

""", status_code=403) async def set_title(request: Request): decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: return HTMLResponse("""

Invalid request data

""", status_code=400) should_serve = True if AUTHORIZATION_NEEDED: should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() page_id = decrypted_fields[b'page_id'][0].decode() title_id = decrypted_fields[b'title_id'][0].decode() current_title = 1 query = select(daily_reward.c.title).where(daily_reward.c.device_id == device_id) row = await database.fetch_one(query) if row: current_title = row["title"] confirm_url = encryptAES( f"vid={device_id}&page_id={page_id}&set_title={title_id}&dummy=".encode("utf-8") ) go_back_url = encryptAES( f"vid={device_id}&page_id={page_id}&dummy=".encode("utf-8") ) html = f"""

Would you like to change your title?
Current Title:

Current Title

New Title:

New Title
Confirm Go back
""" return HTMLResponse(inform_page(html, 1)) else: return HTMLResponse("""

Access denied

""", status_code=403) async def mission(request: Request): decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: return HTMLResponse("""

Invalid request data

""", status_code=400) should_serve = True if AUTHORIZATION_NEEDED: should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: html = f"""
Play Music to level up and unlock free songs!
Songs can only be unlocked when you play online.
""" for song in EXP_UNLOCKED_SONGS: song_id = song["id"] level_required = song["lvl"] song_name = SONG_LIST[song_id]["name_en"] if song_id < len(SONG_LIST) else "Unknown Song" html += f"""
Level {level_required}
{song_name}
""" html += "
" file_path = os.path.join("files", "mission.html") try: with open(file_path, "r", encoding="utf-8") as file: html_content = file.read().format(text=html) except FileNotFoundError: return HTMLResponse("""

Mission file not found

""", status_code=500) return HTMLResponse(html_content) else: return HTMLResponse("""

Access denied

""", status_code=403) routes = [ Route('/ranking.php', ranking, methods=['GET']), Route('/ranking_detail.php', ranking_detail, methods=['GET']), Route('/set_title.php', set_title, methods=['GET']), Route('/mission.php', mission, methods=['GET']), Route('/status.php', status, methods=['GET']), ]