forked from GuChen/maimaiDX-API-Web-Server
555 lines
19 KiB
Python
555 lines
19 KiB
Python
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)}
|