forked from GuChen/maimaiDX-API-Web-Server
Initial commit: Add maimaiDX API web application with AimeDB scanning and logging features
This commit is contained in:
554
backend/main.py
Normal file
554
backend/main.py
Normal file
@@ -0,0 +1,554 @@
|
||||
import uvicorn
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Body
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from loguru import logger
|
||||
from typing import Annotated
|
||||
|
||||
# 创建logs目录(如果不存在)
|
||||
if not os.path.exists("logs"):
|
||||
os.makedirs("logs")
|
||||
|
||||
# 配置日志
|
||||
logger.add("logs/app_{time}.log", rotation="100 MB", retention="30 days")
|
||||
logger.add("logs/error_{time}.log", level="ERROR", rotation="100 MB", retention="90 days")
|
||||
|
||||
# API Imports
|
||||
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||
from GetUserData import apiGetUserData
|
||||
from HelperUserAll import generateFullUserAll
|
||||
from HelperGetUserMusicDetail import getUserFullMusicDetail
|
||||
from Best50_To_Diving_Fish import implUserMusicToDivingFish
|
||||
from GetPreview import apiGetUserPreview
|
||||
from ActionLoginBonus import implLoginBonus
|
||||
from ActionUnlockItem import implUnlockMultiItem
|
||||
from ActionScoreRecord import implUploadMusicRecord, implDeleteMusicRecord
|
||||
from ChargeTicket import implBuyTicket, implWipeTickets
|
||||
from GetAny import apiGetAny
|
||||
from API_AuthLiteDelivery import getRawDelivery, parseRawDelivery, getUpdateIniFromURL, parseUpdateIni
|
||||
from API_AimeDB import implGetUID
|
||||
from APILogger import api_logger
|
||||
import uvicorn
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Body
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from loguru import logger
|
||||
from typing import Annotated
|
||||
|
||||
# 创建logs目录(如果不存在)
|
||||
if not os.path.exists("logs"):
|
||||
os.makedirs("logs")
|
||||
|
||||
# 配置日志
|
||||
logger.add("logs/app_{time}.log", rotation="100 MB", retention="30 days")
|
||||
logger.add("logs/error_{time}.log", level="ERROR", rotation="100 MB", retention="90 days")
|
||||
|
||||
# API Imports
|
||||
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||
from GetUserData import apiGetUserData
|
||||
from HelperUserAll import generateFullUserAll
|
||||
from HelperGetUserMusicDetail import getUserFullMusicDetail
|
||||
from Best50_To_Diving_Fish import implUserMusicToDivingFish
|
||||
from GetPreview import apiGetUserPreview
|
||||
from ActionLoginBonus import implLoginBonus
|
||||
from ActionUnlockItem import implUnlockMultiItem
|
||||
from ActionScoreRecord import implUploadMusicRecord, implDeleteMusicRecord
|
||||
from ChargeTicket import implBuyTicket, implWipeTickets
|
||||
from GetAny import apiGetAny
|
||||
from API_AuthLiteDelivery import getRawDelivery, parseRawDelivery, getUpdateIniFromURL, parseUpdateIni
|
||||
from API_AimeDB import implGetUID
|
||||
from APILogger import api_logger
|
||||
from MyConfig import testUid8
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 日志中间件
|
||||
@app.middleware("http")
|
||||
async def log_requests(request, call_next):
|
||||
# 记录请求开始时间
|
||||
start_time = datetime.now()
|
||||
|
||||
# 记录请求信息
|
||||
logger.info(f"Request: {request.method} {request.url} - Headers: {dict(request.headers)}")
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
# 记录响应信息和处理时间
|
||||
process_time = (datetime.now() - start_time).total_seconds()
|
||||
logger.info(f"Response: {response.status_code} - Process Time: {process_time}s")
|
||||
|
||||
return response
|
||||
|
||||
# Set up CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/api")
|
||||
async def root():
|
||||
return {"message": "Welcome to the maimaiDX API"}
|
||||
|
||||
@app.get("/api/music")
|
||||
async def get_music():
|
||||
try:
|
||||
with open("Data/musicDB.json", "r", encoding="utf-8") as f:
|
||||
music_data_obj = json.load(f)
|
||||
|
||||
# Convert the object to a list of objects
|
||||
music_list = [
|
||||
{
|
||||
"id": int(music_id),
|
||||
"name": details.get("name"),
|
||||
"version": details.get("version")
|
||||
}
|
||||
for music_id, details in music_data_obj.items()
|
||||
]
|
||||
return music_list
|
||||
|
||||
except FileNotFoundError:
|
||||
return {"error": "Music database not found."}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@app.get("/api/user/all")
|
||||
async def get_user_all(userId: int = None):
|
||||
if userId is None:
|
||||
userId = testUid8
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Get basic user data
|
||||
user_data_str = apiGetUserData(userId, noLog=True)
|
||||
user_data = json.loads(user_data_str)
|
||||
|
||||
if "userData" not in user_data:
|
||||
return {"error": "GetUserData failed", "details": user_data}
|
||||
current_user_data2 = user_data["userData"]
|
||||
|
||||
# 3. Generate the full UserAll object
|
||||
full_user_all = generateFullUserAll(
|
||||
userId,
|
||||
login_result,
|
||||
timestamp,
|
||||
current_user_data2,
|
||||
{}
|
||||
)
|
||||
|
||||
# 4. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return full_user_all
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in get_user_all: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.get("/api/user/music_results")
|
||||
async def get_user_music_results(userId: int):
|
||||
if not userId:
|
||||
return {"error": "userId is required"}
|
||||
try:
|
||||
# This might be slow as it fetches all pages
|
||||
full_music_details = getUserFullMusicDetail(userId)
|
||||
return {"userMusicResults": full_music_details}
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in get_user_music_results: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/user/upload_to_diving_fish")
|
||||
async def upload_to_diving_fish(
|
||||
data: dict
|
||||
):
|
||||
userId = data.get("userId")
|
||||
fishImportToken = data.get("fishImportToken")
|
||||
if not userId or not fishImportToken:
|
||||
return {"error": "userId and fishImportToken are required"}
|
||||
try:
|
||||
result_code = implUserMusicToDivingFish(userId, fishImportToken)
|
||||
error_map = {
|
||||
0: "Success",
|
||||
1: "Failed to get user music from game server.",
|
||||
2: "Diving Fish authentication failed. Check your token.",
|
||||
3: "Communication error with Diving Fish server."
|
||||
}
|
||||
return {
|
||||
"resultCode": result_code,
|
||||
"message": error_map.get(result_code, "Unknown error")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in upload_to_diving_fish: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.get("/api/user/preview")
|
||||
async def get_user_preview(userId: int):
|
||||
if not userId:
|
||||
return {"error": "userId is required"}
|
||||
try:
|
||||
preview_data_str = apiGetUserPreview(userId, noLog=True)
|
||||
return json.loads(preview_data_str)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in get_user_preview: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/claim_login_bonus")
|
||||
async def claim_login_bonus(data: dict):
|
||||
userId = data.get("userId")
|
||||
if not userId:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Claim bonus
|
||||
# Using mode 2 to claim all available bonuses
|
||||
bonus_result = implLoginBonus(userId, timestamp, login_result, bonusGenerateMode=2)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
if bonus_result:
|
||||
return {"message": "Login bonus action completed successfully.", "details": bonus_result}
|
||||
else:
|
||||
return {"message": "No applicable login bonus found or action failed."}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in claim_login_bonus: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/unlock_item")
|
||||
async def unlock_item(data: dict):
|
||||
userId = data.get("userId")
|
||||
itemKind = data.get("itemKind")
|
||||
itemIds_str = data.get("itemIds") # Expecting a comma-separated string of integers
|
||||
|
||||
if not all([userId, itemKind is not None, itemIds_str]):
|
||||
return {"error": "userId, itemKind, and a string of itemIds are required"}
|
||||
|
||||
try:
|
||||
itemIds = [int(i.strip()) for i in itemIds_str.split(",")]
|
||||
except ValueError:
|
||||
return {"error": "itemIds must be a comma-separated string of numbers"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Unlock items
|
||||
unlock_result = implUnlockMultiItem(
|
||||
itemKind, userId, timestamp, login_result, *itemIds
|
||||
)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return {"message": "Unlock action completed.", "details": unlock_result}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in unlock_item: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/upload_score")
|
||||
async def upload_score(data: dict):
|
||||
userId = data.get("userId")
|
||||
musicId = data.get("musicId")
|
||||
levelId = data.get("levelId")
|
||||
achievement = data.get("achievement")
|
||||
dxScore = data.get("dxScore")
|
||||
|
||||
if not all([userId, musicId, levelId is not None, achievement is not None, dxScore is not None]):
|
||||
return {"error": "userId, musicId, levelId, achievement, and dxScore are required"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Upload score
|
||||
upload_result = implUploadMusicRecord(
|
||||
userId, timestamp, login_result, musicId, levelId, achievement, dxScore
|
||||
)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return {"message": "Score upload action completed.", "details": upload_result}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in upload_score: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/delete_score")
|
||||
async def delete_score(data: dict):
|
||||
userId = data.get("userId")
|
||||
musicId = data.get("musicId")
|
||||
levelId = data.get("levelId")
|
||||
|
||||
if not all([userId, musicId, levelId is not None]):
|
||||
return {"error": "userId, musicId, and levelId are required"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Delete score
|
||||
delete_result = implDeleteMusicRecord(
|
||||
userId, timestamp, login_result, musicId, levelId
|
||||
)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return {"message": "Score delete action completed.", "details": delete_result}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in delete_score: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/buy_ticket")
|
||||
async def buy_ticket(data: dict):
|
||||
userId = data.get("userId")
|
||||
ticketType = data.get("ticketType")
|
||||
if not all([userId, ticketType is not None]):
|
||||
return {"error": "userId and ticketType are required"}
|
||||
|
||||
try:
|
||||
ticketType = int(ticketType)
|
||||
except ValueError:
|
||||
return {"error": "ticketType must be a number"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Buy ticket
|
||||
buy_result = implBuyTicket(userId, ticketType)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return {"message": "Buy ticket action completed.", "details": json.loads(buy_result)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in buy_ticket: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/force_logout")
|
||||
async def force_logout(data: dict):
|
||||
userId = data.get("userId")
|
||||
if not userId:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
try:
|
||||
# Attempt to logout with the current timestamp
|
||||
timestamp = generateTimestamp()
|
||||
logout_result = apiLogout(timestamp, userId, noLog=True)
|
||||
return {"message": "Force logout attempt sent.", "details": logout_result}
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in force_logout: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/action/wipe_tickets")
|
||||
async def wipe_tickets(data: dict):
|
||||
userId = data.get("userId")
|
||||
if not userId:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
timestamp = None
|
||||
try:
|
||||
# 1. Login
|
||||
timestamp = generateTimestamp()
|
||||
login_result = apiLogin(timestamp, userId)
|
||||
if login_result.get("returnCode") != 1:
|
||||
return {"error": "Login failed", "details": login_result}
|
||||
|
||||
# 2. Wipe tickets
|
||||
wipe_result = implWipeTickets(userId, timestamp, login_result)
|
||||
|
||||
# 3. Logout
|
||||
apiLogout(timestamp, userId)
|
||||
|
||||
return {"message": "Wipe tickets action completed.", "details": wipe_result}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in wipe_tickets: {e}")
|
||||
if timestamp and userId:
|
||||
try:
|
||||
apiLogout(timestamp, userId)
|
||||
except Exception as logout_e:
|
||||
logger.error(f"Error during cleanup logout: {logout_e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
@app.get("/api/get_auth_lite_delivery")
|
||||
async def get_auth_lite_delivery():
|
||||
try:
|
||||
raw_delivery_str = getRawDelivery()
|
||||
update_links = parseRawDelivery(raw_delivery_str)
|
||||
return {"updateLinks": update_links}
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in get_auth_lite_delivery: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.get("/api/get_any")
|
||||
async def get_any(userId: int, apiName: str):
|
||||
if not all([userId, apiName]):
|
||||
return {"error": "userId and apiName are required"}
|
||||
try:
|
||||
result_str = apiGetAny(userId, apiName, noLog=True)
|
||||
return json.loads(result_str)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred in get_any for {apiName}: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/parse_update_ini")
|
||||
async def parse_update_ini(data: dict):
|
||||
url = data.get("url")
|
||||
if not url:
|
||||
return {"error": "URL is required"}
|
||||
try:
|
||||
ini_text = getUpdateIniFromURL(url)
|
||||
parsed_data = parseUpdateIni(ini_text)
|
||||
return {"parsedData": parsed_data}
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred parsing INI from {url}: {e}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.post("/api/aime_scan")
|
||||
async def aime_scan(data: dict):
|
||||
qr_content = data.get("qrContent")
|
||||
if not qr_content:
|
||||
api_logger.log_warning("Aime scan failed: qrContent is required")
|
||||
return {"error": "qrContent is required"}
|
||||
try:
|
||||
api_logger.log_info(f"Aime scan request received for QR content: {qr_content[:20]}...")
|
||||
result = implGetUID(qr_content)
|
||||
api_logger.log_info(f"Aime scan completed successfully for QR content: {qr_content[:20]}...")
|
||||
if "errorID" in result:
|
||||
api_logger.log_warning(f"Aime scan returned error: {result['errorID']}")
|
||||
elif "userID" in result:
|
||||
api_logger.log_info(f"Aime scan returned user ID: {result['userID']}")
|
||||
return result
|
||||
except Exception as e:
|
||||
api_logger.log_error(f"An error occurred in aime_scan: {str(e)}")
|
||||
return {"error": "An unexpected error occurred.", "details": str(e)}
|
||||
|
||||
@app.get("/api/logs")
|
||||
async def get_logs():
|
||||
"""获取日志文件内容"""
|
||||
try:
|
||||
import os
|
||||
import glob
|
||||
from datetime import datetime
|
||||
|
||||
# 获取最新的日志文件
|
||||
log_files = glob.glob("logs/app_*.log")
|
||||
if not log_files:
|
||||
return {"logs": []}
|
||||
|
||||
# 按修改时间排序,获取最新的日志文件
|
||||
latest_log_file = max(log_files, key=os.path.getmtime)
|
||||
|
||||
# 读取日志文件内容
|
||||
with open(latest_log_file, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# 解析日志行
|
||||
logs = []
|
||||
for line in lines[-100:]: # 只返回最后100行
|
||||
# 尝试解析loguru格式的日志
|
||||
try:
|
||||
# 格式: 2023-05-15 10:30:45.123 | INFO | message
|
||||
parts = line.strip().split(" | ")
|
||||
if len(parts) >= 3:
|
||||
timestamp_str = parts[0]
|
||||
level = parts[1].strip()
|
||||
message = " | ".join(parts[2:])
|
||||
|
||||
# 解析时间戳
|
||||
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
logs.append({
|
||||
"timestamp": timestamp.isoformat(),
|
||||
"level": level,
|
||||
"message": message
|
||||
})
|
||||
except Exception:
|
||||
# 如果解析失败,将整行作为消息
|
||||
logs.append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": "INFO",
|
||||
"message": line.strip()
|
||||
})
|
||||
|
||||
return {"logs": logs}
|
||||
except Exception as e:
|
||||
api_logger.log_error(f"Failed to read logs: {str(e)}")
|
||||
return {"error": "Failed to read logs", "details": str(e)}
|
||||
Reference in New Issue
Block a user