Redis support for linux

This commit is contained in:
UnitedAirforce
2025-06-28 08:18:18 +08:00
parent b47f9296d4
commit f8c322ab6f
6 changed files with 125 additions and 114 deletions

View File

@@ -15,7 +15,7 @@ from api.ranking import routes as rank_routes
from api.shop import routes as shop_routes from api.shop import routes as shop_routes
from api.play import routes as play_routes from api.play import routes as play_routes
from config import HOST, PORT, DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT from config import DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT
if (os.path.isfile('./files/dlc_4max.html')): if (os.path.isfile('./files/dlc_4max.html')):
get_4max_version_string() get_4max_version_string()
@@ -42,6 +42,7 @@ app = Starlette(debug=DEBUG, routes=routes)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
global redis
await database.connect() await database.connect()
await init_db() await init_db()

View File

@@ -1,14 +1,17 @@
from starlette.responses import JSONResponse, Response
from starlette.requests import Request
import sqlalchemy import sqlalchemy
from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey from sqlalchemy import Table, Column, Integer, String, DateTime
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import select, update from sqlalchemy import select, update
from config import REDIS_ADDRESS, USE_REDIS_CACHE
import os import os
import databases import databases
import datetime import datetime
if USE_REDIS_CACHE:
import redis.asyncio as aioredis
redis = None
DB_NAME = "player.db" DB_NAME = "player.db"
DB_PATH = os.path.join(os.getcwd(), DB_NAME) DB_PATH = os.path.join(os.getcwd(), DB_NAME)
@@ -81,7 +84,7 @@ blacklist = Table(
) )
async def init_db(): async def init_db():
global redis
if not os.path.exists(DB_PATH): if not os.path.exists(DB_PATH):
print("[DB] Creating new database:", DB_PATH) print("[DB] Creating new database:", DB_PATH)
@@ -93,6 +96,9 @@ async def init_db():
await engine.dispose() await engine.dispose()
print("[DB] Database initialized successfully.") print("[DB] Database initialized successfully.")
await ensure_user_columns() await ensure_user_columns()
if USE_REDIS_CACHE:
print("[DB] Connecting to Redis at", REDIS_ADDRESS)
redis = await aioredis.from_url("redis://" + REDIS_ADDRESS)
async def get_user_data(uid, data_field): async def get_user_data(uid, data_field):
query = select(user.c[data_field]).where(user.c.device_id == uid[b'vid'][0].decode()) query = select(user.c[data_field]).where(user.c.device_id == uid[b'vid'][0].decode())

View File

@@ -2,11 +2,12 @@ from starlette.responses import HTMLResponse
from starlette.requests import Request from starlette.requests import Request
from starlette.routing import Route from starlette.routing import Route
import os import os
import json
from sqlalchemy import select, update from sqlalchemy import select, update
from config import AUTHORIZATION_NEEDED from config import AUTHORIZATION_NEEDED, USE_REDIS_CACHE
from api.database import database, user, result, daily_reward, check_blacklist, check_whitelist import api.database
from api.crypt import decrypt_fields, encryptAES from api.crypt import decrypt_fields, encryptAES
from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST
from api.misc import inform_page from api.misc import inform_page
@@ -18,7 +19,7 @@ async def ranking(request: Request):
should_serve = True should_serve = True
if AUTHORIZATION_NEEDED: if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields)
if should_serve: if should_serve:
device_id = decrypted_fields[b'vid'][0].decode() device_id = decrypted_fields[b'vid'][0].decode()
@@ -60,7 +61,7 @@ async def ranking_detail(request: Request):
should_serve = True should_serve = True
if AUTHORIZATION_NEEDED: if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields)
if should_serve: if should_serve:
device_id = decrypted_fields[b'vid'][0].decode() device_id = decrypted_fields[b'vid'][0].decode()
@@ -108,6 +109,11 @@ async def ranking_detail(request: Request):
play_results = None play_results = None
user_result = None user_result = None
device_result = None device_result = None
if USE_REDIS_CACHE:
cache_key = f"{song_id}-{mode}"
cached = await api.database.redis.get(cache_key)
else:
cached = False
if (song_id == -1): if (song_id == -1):
# Filter out the mobile/AC modes # Filter out the mobile/AC modes
@@ -118,50 +124,57 @@ async def ranking_detail(request: Request):
else: else:
exclude = [1, 2, 3] exclude = [1, 2, 3]
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) if cached and USE_REDIS_CACHE:
cur_user = await database.fetch_one(query) sorted_players = json.loads(cached)
player_scores = {} else:
query = select(api.database.result.c.vid, api.database.result.c.sid, api.database.result.c.mode, api.database.result.c.avatar, api.database.result.c.score)
play_results = await api.database.database.fetch_all(query)
filtered_play_results = [play for play in play_results if int(play[2]) not in exclude] query = select(api.database.daily_reward.c.device_id, api.database.daily_reward.c.title, api.database.daily_reward.c.avatar)
device_results_raw = await api.database.database.fetch_all(query)
device_results = {row["device_id"]: {"title": row["title"], "avatar": row["avatar"]} for row in device_results_raw}
for play in filtered_play_results: query = select(api.database.user.c.id, api.database.user.c.username, api.database.user.c.device_id)
did = play[0] user_results_raw = await api.database.database.fetch_all(query)
sid = play[1] user_results = {row["id"]: {"username": row["username"], "device_id": row["device_id"]} for row in user_results_raw}
avatar = play[3]
score = play[4] query = select(api.database.user).where(api.database.user.c.device_id == device_id)
username, title = None, None cur_user = await api.database.database.fetch_one(query)
if sid: player_scores = {}
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 filtered_play_results = [play for play in play_results if int(play[2]) not in exclude]
title = device_results.get(did, {}).get("title", "1")
if username in player_scores: for play in filtered_play_results:
player_scores[username]["score"] += int(score) did = play[0]
player_scores[username]["avatar"] = avatar # But avatar is based on latest play submission sid = play[1]
player_scores[username]["title"] = title avatar = play[3]
else: score = play[4]
player_scores[username] = {"score": int(score), "avatar": avatar, "title": title} username, title = None, None
sorted_players = sorted(player_scores.items(), key=lambda x: x[1]["score"], reverse=True) 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 api.database.redis.set(cache_key, json.dumps(sorted_players), ex=300)
username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})" username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})"
@@ -214,15 +227,21 @@ async def ranking_detail(request: Request):
""" """
else: else:
query = select(result).where((result.c.id == song_id) & (result.c.mode == mode)) if cached and USE_REDIS_CACHE:
play_results = await database.fetch_all(query) play_results = json.loads(cached)
play_results = sorted(play_results, key=lambda x: int(x[8]), reverse=True)
query = select(user).where(user.c.device_id == device_id) else:
user_result = await database.fetch_one(query) query = select(api.database.result).where((api.database.result.c.id == song_id) & (api.database.result.c.mode == mode))
play_results = await api.database.database.fetch_all(query)
play_results = sorted(play_results, key=lambda x: int(x[8]), reverse=True)
if USE_REDIS_CACHE:
await api.database.redis.set(cache_key, json.dumps(play_results), ex=300)
query = select(daily_reward).where(daily_reward.c.device_id == device_id) query = select(api.database.user).where(api.database.user.c.device_id == device_id)
device_result = await database.fetch_one(query) user_result = await api.database.database.fetch_one(query)
query = select(api.database.daily_reward).where(api.database.daily_reward.c.device_id == device_id)
device_result = await api.database.database.fetch_one(query)
user_id = user_result[0] if user_result else None user_id = user_result[0] if user_result else None
username = user_result[1] if user_result else f"Guest({device_id[-6:]})" username = user_result[1] if user_result else f"Guest({device_id[-6:]})"
@@ -265,13 +284,13 @@ async def ranking_detail(request: Request):
username = f"Guest({record[1][-6:]})" username = f"Guest({record[1][-6:]})"
device_info = None device_info = None
if record[3]: if record[3]:
query = select(user.c.username).where(user.c.id == record[3]) query = select(api.database.user.c.username).where(api.database.user.c.id == record[3])
user_data = await database.fetch_one(query) user_data = await api.database.database.fetch_one(query)
if user_data: if user_data:
username = user_data["username"] username = user_data["username"]
query = select(daily_reward.c.title).where(daily_reward.c.device_id == record[1]) query = select(api.database.daily_reward.c.title).where(api.database.daily_reward.c.device_id == record[1])
device_title = await database.fetch_one(query) device_title = await api.database.database.fetch_one(query)
if device_title: if device_title:
device_info = device_title["title"] device_info = device_title["title"]
else: else:
@@ -321,7 +340,7 @@ async def status(request: Request):
should_serve = True should_serve = True
if AUTHORIZATION_NEEDED: if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields)
if should_serve: if should_serve:
device_id = decrypted_fields[b'vid'][0].decode() device_id = decrypted_fields[b'vid'][0].decode()
@@ -330,19 +349,19 @@ async def status(request: Request):
if set_title: if set_title:
update_query = ( update_query = (
update(daily_reward) update(api.database.daily_reward)
.where(daily_reward.c.device_id == device_id) .where(api.database.daily_reward.c.device_id == device_id)
.values(title=set_title) .values(title=set_title)
) )
await database.execute(update_query) await api.database.execute(update_query)
query = select(daily_reward).where(daily_reward.c.device_id == device_id) query = select(api.database.daily_reward).where(api.database.daily_reward.c.device_id == device_id)
user_data = await database.fetch_one(query) user_data = await api.database.database.fetch_one(query)
user_name = f"Guest({device_id[-6:]})" user_name = f"Guest({device_id[-6:]})"
if user_data: if user_data:
query = select(user.c.username).where(user.c.device_id == device_id) query = select(api.database.user.c.username).where(api.database.user.c.device_id == device_id)
user_result = await database.fetch_one(query) user_result = await api.database.database.fetch_one(query)
if user_result: if user_result:
user_name = user_result["username"] user_name = user_result["username"]
@@ -416,7 +435,7 @@ async def set_title(request: Request):
should_serve = True should_serve = True
if AUTHORIZATION_NEEDED: if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields)
if should_serve: if should_serve:
device_id = decrypted_fields[b'vid'][0].decode() device_id = decrypted_fields[b'vid'][0].decode()
@@ -424,8 +443,8 @@ async def set_title(request: Request):
title_id = decrypted_fields[b'title_id'][0].decode() title_id = decrypted_fields[b'title_id'][0].decode()
current_title = 1 current_title = 1
query = select(daily_reward.c.title).where(daily_reward.c.device_id == device_id) query = select(api.database.daily_reward.c.title).where(api.database.daily_reward.c.device_id == device_id)
row = await database.fetch_one(query) row = await api.database.database.fetch_one(query)
if row: if row:
current_title = row["title"] current_title = row["title"]
@@ -458,7 +477,7 @@ async def mission(request: Request):
should_serve = True should_serve = True
if AUTHORIZATION_NEEDED: if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields)
if should_serve: if should_serve:
html = f"""<div class="f90 a_center pt50">Play Music to level up and unlock free songs!<br>Songs can only be unlocked when you play online.</div><div class='mission-list'>""" html = f"""<div class="f90 a_center pt50">Play Music to level up and unlock free songs!<br>Songs can only be unlocked when you play online.</div><div class='mission-list'>"""

View File

@@ -279,7 +279,7 @@ async def load(request: Request):
async def save(request: Request): async def save(request: Request):
decrypted_fields, _ = await decrypt_fields(request) decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields: if not decrypted_fields:
return Response("""<response><code>10</code></response>""", media_type="application/xml") return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
data = await request.body() data = await request.body()
data = data.decode("utf-8") data = data.decode("utf-8")
@@ -306,7 +306,7 @@ async def save(request: Request):
return Response("""<response><code>0</code></response>""", media_type="application/xml") return Response("""<response><code>0</code></response>""", media_type="application/xml")
else: else:
return Response("""<response><code>10</code></response>""", media_type="application/xml") return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
async def start(request: Request): async def start(request: Request):
decrypted_fields, _ = await decrypt_fields(request) decrypted_fields, _ = await decrypt_fields(request)

View File

@@ -1,18 +0,0 @@
HOST = 192.168.0.106
PORT = 9068
ACTUAL_HOST = 192.168.0.106
ACTUAL_PORT = 9068
MODEL = 202504125800
TUNEFILE = 202504125800
SKIN = 202404191149
STAGE_PRICE = 1
AVATAR_PRICE = 1
ITEM_PRICE = 2
COIN_REWARD = 1
START_COIN = 10
AUTHORIZATION_NEEDED = False
DEBUG = True

View File

@@ -1,63 +1,66 @@
from starlette.config import Config
import os import os
''' '''
Do not change the name of this file. Do not change the name of this file.
不要改动这个文件的名称。 不要改动这个文件的名称。
''' '''
config = Config("config.env")
''' '''
IP and port of the server. IP and port of the server.
服务器的IP和端口。 服务器的IP和端口。
''' '''
HOST = config("HOST", default="192.168.0.106") HOST = "192.168.0.106"
PORT = int(config("PORT", default=9070)) PORT = 9070
ACTUAL_HOST = config("ACTUAL_HOST", default="192.168.0.106") ACTUAL_HOST = "192.168.0.106"
ACTUAL_PORT = int(config("ACTUAL_PORT", default=9070)) ACTUAL_PORT = 9068
'''
Redis leaderboard caching configuration.
Redis排行榜缓存设置。
'''
REDIS_ADDRESS = "localhost"
USE_REDIS_CACHE = False
''' '''
Datecode of the 3 pak files. Datecode of the 3 pak files.
三个pak文件的时间戳。 三个pak文件的时间戳。
''' '''
MODEL = config("MODEL", cast=str, default="202504125800") MODEL = "202504125800"
TUNEFILE = config("TUNEFILE", cast=str, default="202504125800") TUNEFILE = "202504125800"
SKIN = config("SKIN", cast=str, default="202404191149") SKIN = "202404191149"
''' '''
Groove Coin-related settings. Groove Coin-related settings.
GCoin相关设定。 GCoin相关设定。
''' '''
STAGE_PRICE = 1
STAGE_PRICE = config("STAGE_PRICE", cast=int, default=1) AVATAR_PRICE = 1
AVATAR_PRICE = config("AVATAR_PRICE", cast=int, default=1) ITEM_PRICE = 2
ITEM_PRICE = config("ITEM_PRICE", cast=int, default=2) COIN_REWARD = 1
COIN_REWARD = config("COIN_REWARD", cast=int, default=1) START_COIN = 10
START_COIN = config("START_COIN", cast=int, default=10)
''' '''
Only the whitelisted playerID can use the service. Blacklist has priority over whitelist. Only the whitelisted playerID can use the service. Blacklist has priority over whitelist.
只有白名单的玩家ID才能使用服务。黑名单优先于白名单。 只有白名单的玩家ID才能使用服务。黑名单优先于白名单。
''' '''
AUTHORIZATION_NEEDED = False
AUTHORIZATION_NEEDED = config("AUTHORIZATION_NEEDED", cast=bool, default=False)
''' '''
SSL证书路径 - 留空则使用HTTP SSL证书路径 - 留空则使用HTTP
SSL certificate path. If left blank, use HTTP. SSL certificate path. If left blank, use HTTP.
''' '''
SSL_CERT = config("SSL_CERT", default=None) SSL_CERT = None
SSL_KEY = config("SSL_KEY", default=None) SSL_KEY = None
''' '''
Flask default debug Starlette default debug
Flask内置Debug Starlette内置Debug
''' '''
DEBUG = config("DEBUG", cast=bool, default=False) DEBUG = False
ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__)) ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__))