Initial commit: Add maimaiDX API web application with AimeDB scanning and logging features
This commit is contained in:
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.exe
|
||||||
|
*.dmg
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Python
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Frontend build outputs
|
||||||
|
frontend/dist/
|
||||||
|
frontend/node_modules/
|
||||||
|
|
||||||
|
# Backend build outputs
|
||||||
|
backend/__pycache__/
|
||||||
|
backend/*.pyc
|
||||||
82
README.md
Normal file
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Maimai DX Web 工具
|
||||||
|
|
||||||
|
这是一个基于 Python (FastAPI) 和 Vue.js (Element Plus) 构建的 Web 工具,旨在提供一个用户友好的界面来管理和查看 maimai DX 模拟器的数据。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
maimai-web-app/
|
||||||
|
├── backend/ # Python 后端服务 (FastAPI)
|
||||||
|
└── frontend/ # Vue.js 前端应用 (Element Plus)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行项目
|
||||||
|
|
||||||
|
请确保您的系统已安装 Python 3.8+ 和 Node.js (包含 npm)。
|
||||||
|
|
||||||
|
### 1\. 启动后端服务
|
||||||
|
|
||||||
|
打开您的命令行工具,进入 `maimai-web-app/backend` 目录,然后安装 Python 依赖并启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd maimai-web-app/backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:**
|
||||||
|
|
||||||
|
* 后端服务默认运行在 `http://0.0.0.0:8000`。如果您需要从其他设备访问(例如手机),请确保您的防火墙允许 8000 端口的传入连接,并且您的设备在同一局域网内,或者您已正确配置了端口映射。
|
||||||
|
* `main.py` 中的 `testUid8` 是一个默认的用户ID,您可以在 `MyConfig.py` 中修改它。
|
||||||
|
|
||||||
|
### 2\. 启动前端应用
|
||||||
|
|
||||||
|
打开一个新的命令行工具(不要关闭后端服务的窗口),进入 `maimai-web-app/frontend` 目录,然后安装 Node.js 依赖并启动应用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd maimai-web-app/frontend
|
||||||
|
npm install
|
||||||
|
npm run dev -- --port 5174
|
||||||
|
```
|
||||||
|
|
||||||
|
前端应用默认运行在 `http://localhost:5174`。如果该端口被占用,它会自动尝试其他端口。您也可以通过 `--port` 参数指定端口,例如 `npm run dev -- --port 3000`。
|
||||||
|
|
||||||
|
### 3\. 访问应用
|
||||||
|
|
||||||
|
在浏览器中打开前端应用提供的地址(通常是 `http://localhost:5174`)。
|
||||||
|
|
||||||
|
## 已实现功能
|
||||||
|
|
||||||
|
### 乐曲列表
|
||||||
|
|
||||||
|
* 浏览所有 maimai DX 乐曲。
|
||||||
|
* 支持按歌曲名和艺术家搜索。
|
||||||
|
|
||||||
|
### 用户中心
|
||||||
|
|
||||||
|
* **用户选择**: 通过输入用户ID获取玩家的完整信息或预览信息。
|
||||||
|
* **玩家操作**:
|
||||||
|
|
||||||
|
* **领取登录奖励**: 领取所有可用的每日登录奖励。
|
||||||
|
* **道具管理**: 解锁指定种类和ID的道具。
|
||||||
|
* **票券管理**: 购买指定类型的票券。
|
||||||
|
* **分数管理**: 上传或删除指定乐曲的分数记录。
|
||||||
|
* **上传至水鱼**: 将用户的分数上传至 [diving-fish.com](https://www.diving-fish.com/maimaidx/prober) 进行详细分析。
|
||||||
|
* **通用接口调用**: 通过输入API名称,调用任意 `GetUser...Api` 接口,并显示原始响应。
|
||||||
|
* **危险操作**: 清空所有票券、强制用户登出。
|
||||||
|
|
||||||
|
* **分数详情**: 获取并显示用户的所有乐曲分数记录。
|
||||||
|
|
||||||
|
### 更新链接
|
||||||
|
|
||||||
|
* 获取 AuthLiteDelivery 更新链接列表。
|
||||||
|
* 解析每个更新链接指向的 INI 文件,并美观地展示其中的更新信息(如游戏描述、发布时间、主更新包和可选更新包链接)。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Pull Request 或报告 Bug。在提交代码之前,请确保您的代码符合项目规范。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目采用 MIT 许可证。详情请参阅 `LICENSE` 文件。
|
||||||
|
|
||||||
179
backend/.gitignore
vendored
Normal file
179
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Anti-leak
|
||||||
|
Private_Static_Settings.py
|
||||||
|
MyConfig.py
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
Private_Static_Settings.py
|
||||||
|
Private_Static_Settings.py
|
||||||
|
|
||||||
|
/.python-version
|
||||||
84
backend/APILogger.py
Normal file
84
backend/APILogger.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class APILogger:
|
||||||
|
def __init__(self, log_dir="logs"):
|
||||||
|
self.log_dir = log_dir
|
||||||
|
self._setup_logging()
|
||||||
|
|
||||||
|
def _setup_logging(self):
|
||||||
|
"""设置日志配置"""
|
||||||
|
# 创建日志目录(如果不存在)
|
||||||
|
if not os.path.exists(self.log_dir):
|
||||||
|
os.makedirs(self.log_dir)
|
||||||
|
|
||||||
|
# 配置loguru
|
||||||
|
logger.add(
|
||||||
|
f"{self.log_dir}/app_{{time}}.log",
|
||||||
|
rotation="100 MB",
|
||||||
|
retention="30 days",
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
f"{self.log_dir}/error_{{time}}.log",
|
||||||
|
level="ERROR",
|
||||||
|
rotation="100 MB",
|
||||||
|
retention="90 days",
|
||||||
|
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_request(self, method, url, headers, body=None):
|
||||||
|
"""记录API请求"""
|
||||||
|
log_data = {
|
||||||
|
"type": "request",
|
||||||
|
"method": method,
|
||||||
|
"url": str(url),
|
||||||
|
"headers": dict(headers),
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
if body:
|
||||||
|
log_data["body"] = body
|
||||||
|
|
||||||
|
logger.info(f"API Request: {json.dumps(log_data, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
def log_response(self, status_code, response_data, process_time=None):
|
||||||
|
"""记录API响应"""
|
||||||
|
log_data = {
|
||||||
|
"type": "response",
|
||||||
|
"status_code": status_code,
|
||||||
|
"response_data": response_data,
|
||||||
|
"process_time": process_time,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
if status_code >= 400:
|
||||||
|
logger.error(f"API Response: {json.dumps(log_data, ensure_ascii=False)}")
|
||||||
|
else:
|
||||||
|
logger.info(f"API Response: {json.dumps(log_data, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
def log_error(self, error_msg, error_details=None):
|
||||||
|
"""记录错误"""
|
||||||
|
log_data = {
|
||||||
|
"type": "error",
|
||||||
|
"error_msg": error_msg,
|
||||||
|
"error_details": error_details,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(f"API Error: {json.dumps(log_data, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
def log_info(self, message):
|
||||||
|
"""记录信息"""
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
def log_warning(self, message):
|
||||||
|
"""记录警告"""
|
||||||
|
logger.warning(message)
|
||||||
|
|
||||||
|
|
||||||
|
# 创建全局日志实例
|
||||||
|
api_logger = APILogger()
|
||||||
132
backend/API_AimeDB.py
Normal file
132
backend/API_AimeDB.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# 100% Standalone 的舞萌国服 AimeDB 通讯实现
|
||||||
|
# Ver.CN1.41
|
||||||
|
# Mainline 20250203
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 常量
|
||||||
|
CHIP_ID = "A63E-01E68606624"
|
||||||
|
COMMON_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
|
||||||
|
API_URL = "http://ai.sys-allnet.cn/wc_aime/api/get_data"
|
||||||
|
|
||||||
|
|
||||||
|
# 计算 SHA256
|
||||||
|
def getSHA256(input_str):
|
||||||
|
"""SHA256计算"""
|
||||||
|
return hashlib.sha256(input_str.encode("utf-8")).hexdigest().upper()
|
||||||
|
|
||||||
|
|
||||||
|
# 生成时间戳
|
||||||
|
def generateSEGATimestamp():
|
||||||
|
"""SEGA格式的 YYMMDDHHMMSS 时间戳(sb玩意)"""
|
||||||
|
return time.strftime("%y%m%d%H%M%S", time.localtime())
|
||||||
|
|
||||||
|
|
||||||
|
# 计算认证 key
|
||||||
|
def calcSEGAAimeDBAuthKey(
|
||||||
|
varString: str, timestamp: str, commonKey: str = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
|
||||||
|
) -> str:
|
||||||
|
"""计算 SEGA AimeDB 的认证 key"""
|
||||||
|
return (
|
||||||
|
hashlib.sha256((varString + timestamp + commonKey).encode("utf-8"))
|
||||||
|
.hexdigest()
|
||||||
|
.upper()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apiAimeDB(qrCode):
|
||||||
|
"""AimeDB 扫码 API 实现"""
|
||||||
|
# 生成一个时间戳
|
||||||
|
timestamp = generateSEGATimestamp()
|
||||||
|
|
||||||
|
# 使用时间戳计算 key
|
||||||
|
currentKey = calcSEGAAimeDBAuthKey(CHIP_ID, timestamp, COMMON_KEY)
|
||||||
|
|
||||||
|
# 构造请求数据
|
||||||
|
payload = {
|
||||||
|
"chipID": CHIP_ID,
|
||||||
|
"openGameID": "MAID",
|
||||||
|
"key": currentKey,
|
||||||
|
"qrCode": qrCode,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 输出准备好的请求数据
|
||||||
|
print("Payload:", json.dumps(payload, separators=(",", ":")))
|
||||||
|
|
||||||
|
# 发送 POST 请求
|
||||||
|
headers = {
|
||||||
|
"Connection": "Keep-Alive",
|
||||||
|
"Host": API_URL.split("//")[-1].split("/")[0],
|
||||||
|
"User-Agent": "WC_AIME_LIB",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
response = requests.post(
|
||||||
|
API_URL, data=json.dumps(payload, separators=(",", ":")), headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回服务器的响应
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def isSGWCFormat(input_string: str) -> bool:
|
||||||
|
"""简单检查二维码字符串是否符合格式"""
|
||||||
|
if (
|
||||||
|
len(input_string) != 84 # 长度
|
||||||
|
or not input_string.startswith("SGWCMAID") # 识别字
|
||||||
|
or re.match("^[0-9A-F]+$", input_string[20:]) is None # 有效字符
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def implAimeDB(qrCode: str, isAlreadyFinal: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Aime DB 的请求的参考实现。
|
||||||
|
提供完整 QRCode 内容,返回响应的字符串(Json格式)
|
||||||
|
"""
|
||||||
|
if isAlreadyFinal:
|
||||||
|
qr_code_final = qrCode
|
||||||
|
else:
|
||||||
|
# 提取有效部分(Hash)
|
||||||
|
qr_code_final = qrCode[20:]
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = apiAimeDB(qr_code_final)
|
||||||
|
|
||||||
|
# 获得结果
|
||||||
|
print("implAimeDB: StatusCode is ", response.status_code)
|
||||||
|
print("implAimeDB: Response Body is:", response.text)
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
|
||||||
|
def implGetUID(qr_content: str) -> dict:
|
||||||
|
"""
|
||||||
|
包装后的 UID 扫码器实现。
|
||||||
|
此函数会返回 AimeDB 传回的 Json 转成 Python 字典的结果。
|
||||||
|
主要特点是添加了几个新的错误码(6000x)用来应对程序的错误。
|
||||||
|
"""
|
||||||
|
# 检查格式
|
||||||
|
if not isSGWCFormat(qr_content):
|
||||||
|
return {"errorID": 60001} # 二维码内容明显无效
|
||||||
|
|
||||||
|
# 发送请求并处理响应
|
||||||
|
try:
|
||||||
|
result = json.loads(implAimeDB(qr_content))
|
||||||
|
logger.info(f"QRScan Got Response {result}")
|
||||||
|
except Exception:
|
||||||
|
return {"errorID": 60002} # 无法解码 Response 的内容
|
||||||
|
|
||||||
|
# 返回结果
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userInputQR = input("QRCode: ")
|
||||||
|
print(implAimeDB(userInputQR))
|
||||||
113
backend/API_AuthLiteDelivery.py
Normal file
113
backend/API_AuthLiteDelivery.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# All.Net AuthLite 更新获取
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import pad, unpad
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
import configparser as ini
|
||||||
|
|
||||||
|
LITE_AUTH_KEY = bytes(
|
||||||
|
[47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71]
|
||||||
|
)
|
||||||
|
LITE_AUTH_IV = bytes.fromhex("00000000000000000000000000000000")
|
||||||
|
|
||||||
|
|
||||||
|
def auth_lite_encrypt(plaintext: str) -> bytes:
|
||||||
|
# 构造数据:16字节头 + 16字节0前缀 + 明文
|
||||||
|
content = bytes(32) + plaintext.encode("utf-8")
|
||||||
|
data = content
|
||||||
|
# 填充并加密
|
||||||
|
padded_data = pad(data, AES.block_size)
|
||||||
|
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
|
||||||
|
return cipher.encrypt(padded_data)
|
||||||
|
|
||||||
|
|
||||||
|
def auth_lite_decrypt(ciphertext: bytes) -> str:
|
||||||
|
# 解密并去除填充
|
||||||
|
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
|
||||||
|
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||||
|
# 提取内容并解码
|
||||||
|
content = decrypted_data[16:] # 去除头部的16字节
|
||||||
|
return content.decode("utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def getRawDelivery(title_ver: str = "1.51"):
|
||||||
|
encrypted = auth_lite_encrypt(
|
||||||
|
f"title_id=SDGB&title_ver={title_ver}&client_id=A63E01C2805"
|
||||||
|
)
|
||||||
|
r = httpx.post(
|
||||||
|
"http://at.sys-allnet.cn/net/delivery/instruction",
|
||||||
|
data=encrypted,
|
||||||
|
headers={"User-Agent": "SDGB;Windows/Lite", "Pragma": "DFI"},
|
||||||
|
)
|
||||||
|
resp_data = r.content
|
||||||
|
decrypted_str = auth_lite_decrypt(resp_data)
|
||||||
|
# 过滤所有控制字符
|
||||||
|
decrypted_str = "".join([i for i in decrypted_str if 31 < ord(i) < 127])
|
||||||
|
logger.info(f"RAW Response: {decrypted_str}")
|
||||||
|
|
||||||
|
return decrypted_str
|
||||||
|
|
||||||
|
|
||||||
|
def parseRawDelivery(deliveryStr):
|
||||||
|
"""解析 RAW 的 Delivery 字符串,返回其中的有效的 instruction URL 的列表"""
|
||||||
|
parsedResponseDict: dict[str, str] = {
|
||||||
|
key: value[0] for key, value in parse_qs(deliveryStr).items()
|
||||||
|
}
|
||||||
|
urlList = parsedResponseDict["uri"].split("|")
|
||||||
|
# 过滤掉空字符串和内容为 null 的情况
|
||||||
|
urlList = [url for url in urlList if url and url != "null"]
|
||||||
|
logger.info(f"Parsed URL List: {urlList}")
|
||||||
|
validURLs = []
|
||||||
|
for url in urlList:
|
||||||
|
# 检查是否是 HTTPS 的 URL,以及是否是 txt 文件,否则忽略
|
||||||
|
if not url.startswith("https://") or not url.endswith(".txt"):
|
||||||
|
logger.warning(f"Invalid URL will be ignored: {url}")
|
||||||
|
continue
|
||||||
|
validURLs.append(url)
|
||||||
|
logger.info(f"Verified Valid URLs: {validURLs}")
|
||||||
|
return validURLs
|
||||||
|
|
||||||
|
|
||||||
|
def getUpdateIniFromURL(url):
|
||||||
|
# 发送请求
|
||||||
|
response = httpx.get(
|
||||||
|
url, headers={"User-Agent": "SDGB;Windows/Lite", "Pragma": "DFI"}
|
||||||
|
)
|
||||||
|
logger.info(f"成功自 {url} 获取更新信息")
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
|
||||||
|
def parseUpdateIni(iniText):
|
||||||
|
# 解析配置
|
||||||
|
config = ini.ConfigParser(allow_no_value=True)
|
||||||
|
config.read_string(iniText)
|
||||||
|
logger.info(f"成功解析配置文件,包含的节有:{config.sections()}")
|
||||||
|
|
||||||
|
# 获取 COMMON 节的配置
|
||||||
|
common = config["COMMON"]
|
||||||
|
|
||||||
|
game_desc = common.get("GAME_DESC", "").strip('"')
|
||||||
|
release_time = common.get("RELEASE_TIME", "").replace("T", " ")
|
||||||
|
main_file = common.get("INSTALL1", "")
|
||||||
|
|
||||||
|
optional_files = []
|
||||||
|
if "OPTIONAL" in config:
|
||||||
|
for key, url in config.items("OPTIONAL"):
|
||||||
|
optional_files.append(f"{url.split('/')[-1]} {url}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"game_desc": game_desc,
|
||||||
|
"release_time": release_time,
|
||||||
|
"main_file": main_file,
|
||||||
|
"optional_files": optional_files,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raw = getRawDelivery("1.51")
|
||||||
|
urlList = parseRawDelivery(raw)
|
||||||
|
for url in urlList:
|
||||||
|
iniText = getUpdateIniFromURL(url)
|
||||||
|
message = parseUpdateIni(iniText)
|
||||||
352
backend/API_TitleServer.py
Normal file
352
backend/API_TitleServer.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# 舞萌DX
|
||||||
|
# 标题服务器通讯实现
|
||||||
|
import zlib
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
from ctypes import c_int32
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import pad, unpad
|
||||||
|
|
||||||
|
from Config import (
|
||||||
|
useProxy,
|
||||||
|
proxyUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"
|
||||||
|
AesIV = "d6xHIKq]1J]Dt^ue"
|
||||||
|
ObfuscateParam = "B44df8yT"
|
||||||
|
|
||||||
|
|
||||||
|
def use2024Api():
|
||||||
|
global AesKey, AesIV, ObfuscateParam
|
||||||
|
|
||||||
|
AesKey = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"
|
||||||
|
AesIV = ";;KjR1C3hgB1ovXa"
|
||||||
|
ObfuscateParam = "BEs2D5vW"
|
||||||
|
|
||||||
|
|
||||||
|
class SDGBApiError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SDGBRequestError(SDGBApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SDGBResponseError(SDGBApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class aes_pkcs7(object):
|
||||||
|
def __init__(self, key: str, iv: str):
|
||||||
|
self.key = key.encode("utf-8")
|
||||||
|
self.iv = iv.encode("utf-8")
|
||||||
|
self.mode = AES.MODE_CBC
|
||||||
|
|
||||||
|
def encrypt(self, content: bytes) -> bytes:
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
content_padded = pad(content, AES.block_size)
|
||||||
|
encrypted_bytes = cipher.encrypt(content_padded)
|
||||||
|
return encrypted_bytes
|
||||||
|
|
||||||
|
def decrypt(self, content):
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
decrypted_padded = cipher.decrypt(content)
|
||||||
|
decrypted = unpad(decrypted_padded, AES.block_size)
|
||||||
|
return decrypted
|
||||||
|
|
||||||
|
def pkcs7unpadding(self, text):
|
||||||
|
length = len(text)
|
||||||
|
unpadding = ord(text[length - 1])
|
||||||
|
return text[0 : length - unpadding]
|
||||||
|
|
||||||
|
def pkcs7padding(self, text):
|
||||||
|
bs = 16
|
||||||
|
length = len(text)
|
||||||
|
bytes_length = len(text.encode("utf-8"))
|
||||||
|
padding_size = length if (bytes_length == length) else bytes_length
|
||||||
|
padding = bs - padding_size % bs
|
||||||
|
padding_text = chr(padding) * padding
|
||||||
|
return text + padding_text
|
||||||
|
|
||||||
|
|
||||||
|
def getSDGBApiHash(api):
|
||||||
|
# API 的 Hash 的生成
|
||||||
|
# 有空做一下 Hash 的彩虹表?
|
||||||
|
return hashlib.md5((api + "MaimaiChn" + ObfuscateParam).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def apiSDGB(
|
||||||
|
data: str,
|
||||||
|
targetApi: str,
|
||||||
|
userAgentExtraData: str,
|
||||||
|
noLog: bool = False,
|
||||||
|
timeout: int = 5,
|
||||||
|
maxRetries: int = 3,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
舞萌DX 2025 API 通讯用函数
|
||||||
|
|
||||||
|
:param data: 请求数据
|
||||||
|
:param targetApi: 使用的 API
|
||||||
|
:param userAgentExtraData: UA 附加信息,机台相关则为狗号(如A63E01E9564),用户相关则为 UID
|
||||||
|
:param noLog: 是否不记录日志
|
||||||
|
:param timeout: 请求超时时间(秒)
|
||||||
|
:return: 解码后的响应数据
|
||||||
|
"""
|
||||||
|
# 处理参数
|
||||||
|
agentExtra = str(userAgentExtraData)
|
||||||
|
aes = aes_pkcs7(AesKey, AesIV)
|
||||||
|
endpoint = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/"
|
||||||
|
|
||||||
|
# 准备好请求数据
|
||||||
|
requestDataFinal = aes.encrypt(zlib.compress(data.encode("utf-8")))
|
||||||
|
|
||||||
|
if not noLog:
|
||||||
|
logger.debug(f"[Stage 1] 准备开始请求 {targetApi},以 {data}")
|
||||||
|
|
||||||
|
retries = 0
|
||||||
|
while retries < maxRetries:
|
||||||
|
try:
|
||||||
|
# 配置 HTTP 客户端
|
||||||
|
if useProxy and proxyUrl:
|
||||||
|
logger.debug("使用代理")
|
||||||
|
httpClient = httpx.Client(proxy=proxyUrl, verify=False)
|
||||||
|
else:
|
||||||
|
httpClient = httpx.Client(verify=False)
|
||||||
|
|
||||||
|
api_hash = getSDGBApiHash(targetApi)
|
||||||
|
|
||||||
|
logger.info(f"hash: {api_hash}")
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = httpClient.post(
|
||||||
|
url=endpoint + api_hash,
|
||||||
|
headers={
|
||||||
|
"User-Agent": f"{api_hash}#{agentExtra}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Mai-Encoding": "1.50",
|
||||||
|
"Accept-Encoding": "",
|
||||||
|
"Charset": "UTF-8",
|
||||||
|
"Content-Encoding": "deflate",
|
||||||
|
"Expect": "100-continue",
|
||||||
|
},
|
||||||
|
content=requestDataFinal, # 数据
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not noLog:
|
||||||
|
logger.info(f"[Stage 2] {targetApi} 请求结果: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
errorMessage = f"[Stage 2] 请求失败: {response.status_code}"
|
||||||
|
logger.error(errorMessage)
|
||||||
|
raise SDGBRequestError(errorMessage)
|
||||||
|
|
||||||
|
# 处理响应内容
|
||||||
|
responseContentRaw = response.content
|
||||||
|
|
||||||
|
# 先尝试解密
|
||||||
|
try:
|
||||||
|
responseContentDecrypted = aes.decrypt(responseContentRaw)
|
||||||
|
if not noLog:
|
||||||
|
logger.debug("[Stage 3] Decryption SUCCESS.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[Stage 3] Decryption FAILED. Raw Content: {responseContentRaw}, Error: {e}"
|
||||||
|
)
|
||||||
|
raise SDGBResponseError("Decryption failed")
|
||||||
|
# 然后尝试解压
|
||||||
|
try:
|
||||||
|
# 看看文件头是否是压缩过的
|
||||||
|
if responseContentDecrypted.startswith(b"\x78\x9c"):
|
||||||
|
logger.debug("[Stage 4] Zlib detected, decompressing...")
|
||||||
|
responseContentFinal = zlib.decompress(
|
||||||
|
responseContentDecrypted
|
||||||
|
).decode("utf-8")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"[Stage 4] Not Zlib Format!! using raw content: {responseContentDecrypted}"
|
||||||
|
)
|
||||||
|
responseContentFinal = responseContentDecrypted.decode("utf-8")
|
||||||
|
# 完成解压
|
||||||
|
if not noLog:
|
||||||
|
logger.debug(
|
||||||
|
f"[Stage 4] Process OK, Content: {responseContentFinal}"
|
||||||
|
)
|
||||||
|
# 最终处理,检查是否是 JSON 格式
|
||||||
|
if responseContentFinal.startswith(
|
||||||
|
"{"
|
||||||
|
) and responseContentFinal.endswith("}"):
|
||||||
|
# 如果是 JSON 格式,直接返回
|
||||||
|
logger.debug("[Stage 5] Response is JSON, returning.")
|
||||||
|
return responseContentFinal
|
||||||
|
else:
|
||||||
|
# 如果不是 JSON 格式,直接返回但是警告
|
||||||
|
logger.warning(
|
||||||
|
"[Stage 5] Response is not JSON, returning as is, take care!"
|
||||||
|
)
|
||||||
|
return responseContentFinal
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"解压失败,原始响应: {responseContentDecrypted}")
|
||||||
|
raise SDGBResponseError("解压失败")
|
||||||
|
except SDGBRequestError as e:
|
||||||
|
logger.error(f"请求格式错误: {e}")
|
||||||
|
raise
|
||||||
|
except SDGBResponseError as e:
|
||||||
|
logger.warning(f"响应错误,将重试: {e}")
|
||||||
|
retries += 1
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"请求失败,将重试: {e}")
|
||||||
|
retries += 1
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if "httpClient" in locals():
|
||||||
|
httpClient.close()
|
||||||
|
|
||||||
|
raise SDGBApiError("重试多次仍然无法成功请求服务器")
|
||||||
|
|
||||||
|
|
||||||
|
def calcPlaySpecial():
|
||||||
|
"""使用 c_int32 实现的 SpecialNumber 算法"""
|
||||||
|
rng = random.SystemRandom()
|
||||||
|
num2 = rng.randint(1, 1037933) * 2069
|
||||||
|
num2 += 1024 # GameManager.CalcSpecialNum()
|
||||||
|
num2 = c_int32(num2).value
|
||||||
|
result = c_int32(0)
|
||||||
|
for _ in range(32):
|
||||||
|
result.value <<= 1
|
||||||
|
result.value += num2 & 1
|
||||||
|
num2 >>= 1
|
||||||
|
return c_int32(result.value).value
|
||||||
|
|
||||||
|
|
||||||
|
class AESPKCS7_2024:
|
||||||
|
# 实现了 maimai 通讯所用的 AES 加密的类
|
||||||
|
def __init__(self, key: str, iv: str):
|
||||||
|
self.key = key.encode("utf-8")
|
||||||
|
self.iv = iv.encode("utf-8")
|
||||||
|
self.mode = AES.MODE_CBC
|
||||||
|
|
||||||
|
# 加密
|
||||||
|
def encrypt(self, content) -> bytes:
|
||||||
|
# if content is str, convert to bytes
|
||||||
|
if isinstance(content, str):
|
||||||
|
encodedData = content.encode("utf-8")
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
content_padded = pad(encodedData, AES.block_size)
|
||||||
|
encrypted_bytes = cipher.encrypt(content_padded)
|
||||||
|
return encrypted_bytes
|
||||||
|
|
||||||
|
# 解密
|
||||||
|
def decrypt(self, encrypted_content: bytes) -> str:
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
decrypted_padded = cipher.decrypt(encrypted_content)
|
||||||
|
decrypted = unpad(decrypted_padded, AES.block_size)
|
||||||
|
return decrypted
|
||||||
|
|
||||||
|
|
||||||
|
def apiSDGB_2024(
|
||||||
|
data: str,
|
||||||
|
targetApi: str,
|
||||||
|
userAgentExtraData: str,
|
||||||
|
noLog: bool = False,
|
||||||
|
timeout: int = 5,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
舞萌DX 2024 API 通讯用函数
|
||||||
|
:param data: 请求数据
|
||||||
|
:param targetApi: 使用的 API
|
||||||
|
:param userAgentExtraData: UA 附加信息,机台相关则为狗号(如A63E01E9564),用户相关则为 UID
|
||||||
|
:param noLog: 是否不记录日志
|
||||||
|
"""
|
||||||
|
maxRetries = 3
|
||||||
|
agentExtra = str(userAgentExtraData)
|
||||||
|
aes = AESPKCS7_2024(AesKey, AesIV)
|
||||||
|
reqData_encrypted = aes.encrypt(data)
|
||||||
|
reqData_deflated = zlib.compress(reqData_encrypted)
|
||||||
|
endpoint = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/"
|
||||||
|
if not noLog:
|
||||||
|
logger.debug(f"开始请求 {targetApi},以 {data}")
|
||||||
|
|
||||||
|
retries = 0
|
||||||
|
while retries < maxRetries:
|
||||||
|
try:
|
||||||
|
if useProxy:
|
||||||
|
# 使用代理
|
||||||
|
logger.debug("使用代理")
|
||||||
|
httpClient = httpx.Client(proxy=proxyUrl, verify=False)
|
||||||
|
else:
|
||||||
|
# 不使用代理
|
||||||
|
logger.debug("不使用代理")
|
||||||
|
httpClient = httpx.Client(verify=False)
|
||||||
|
responseOriginal = httpClient.post(
|
||||||
|
url=endpoint + getSDGBApiHash(targetApi),
|
||||||
|
headers={
|
||||||
|
"User-Agent": f"{getSDGBApiHash(targetApi)}#{agentExtra}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Mai-Encoding": "1.40",
|
||||||
|
"Accept-Encoding": "",
|
||||||
|
"Charset": "UTF-8",
|
||||||
|
"Content-Encoding": "deflate",
|
||||||
|
"Expect": "100-continue",
|
||||||
|
},
|
||||||
|
content=reqData_deflated,
|
||||||
|
# 经测试,加 Verify 之后速度慢好多,因此建议选择性开
|
||||||
|
# verify=certifi.where(),
|
||||||
|
# verify=False,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not noLog:
|
||||||
|
logger.info(f"{targetApi} 请求结果: {responseOriginal.status_code}")
|
||||||
|
|
||||||
|
if responseOriginal.status_code == 200:
|
||||||
|
logger.debug("200 OK!")
|
||||||
|
else:
|
||||||
|
errorMessage = f"请求失败: {responseOriginal.status_code}"
|
||||||
|
logger.error(errorMessage)
|
||||||
|
raise SDGBRequestError(errorMessage)
|
||||||
|
|
||||||
|
responseRAWContent = responseOriginal.content
|
||||||
|
|
||||||
|
try:
|
||||||
|
responseDecompressed = zlib.decompress(responseRAWContent)
|
||||||
|
logger.debug("成功解压响应!")
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"无法解压,得到的原始响应: {responseRAWContent}")
|
||||||
|
raise SDGBResponseError("解压失败")
|
||||||
|
try:
|
||||||
|
resultResponse = aes.decrypt(responseDecompressed)
|
||||||
|
logger.debug("成功解密响应!")
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"解密失败,得到的原始响应: {responseDecompressed}")
|
||||||
|
raise SDGBResponseError("解密失败")
|
||||||
|
|
||||||
|
if not noLog:
|
||||||
|
logger.debug(f"响应: {resultResponse}")
|
||||||
|
return resultResponse
|
||||||
|
|
||||||
|
# 异常处理
|
||||||
|
except SDGBRequestError:
|
||||||
|
# 请求格式错误,不需要重试
|
||||||
|
raise SDGBRequestError("请求格式错误")
|
||||||
|
except SDGBResponseError as e:
|
||||||
|
# 响应解析错误,这种有一定可能是我们的问题,所以只重试一次
|
||||||
|
logger.warning(f"将重试一次 Resp Err: {e}")
|
||||||
|
retries += 2
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
# 其他错误,重试多次
|
||||||
|
logger.warning(f"将开始重试请求. {e}")
|
||||||
|
retries += 1
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
raise SDGBApiError("重试多次仍然无法成功请求服务器")
|
||||||
55
backend/ActionChangeVersion.py
Normal file
55
backend/ActionChangeVersion.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 改变版本号,实现伪封号和解封号之类
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperFullPlay import implFullPlayAction, generateMusicData
|
||||||
|
from HelperGetUserThing import implGetUser_
|
||||||
|
from MyConfig import testUid8
|
||||||
|
|
||||||
|
|
||||||
|
def implWipeTickets(userId: int, currentLoginTimestamp: int, currentLoginResult) -> str:
|
||||||
|
# Get User Charge
|
||||||
|
currentUserCharge = implGetUser_("Charge", userId)
|
||||||
|
|
||||||
|
currentUserChargeList = currentUserCharge["userChargeList"]
|
||||||
|
# All stock set to 0
|
||||||
|
for charge in currentUserChargeList:
|
||||||
|
charge["stock"] = 0
|
||||||
|
|
||||||
|
# example format
|
||||||
|
# {"userId":11088995,"length":16,"userChargeList":[{"chargeId":1,"stock":0,"purchaseDate":"2025-02-04 00:51:50","validDate":"2025-02-04 00:51:50","extNum1":0},{"chargeId":2,"stock":0,"purchaseDate":"2025-06-11 17:19:42","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":3,"stock":0,"purchaseDate":"2025-06-11 17:19:40","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":4,"stock":0,"purchaseDate":"2025-06-11 09:34:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":5,"stock":0,"purchaseDate":"2025-01-30 12:31:16","validDate":"2025-04-30 04:00:00","extNum1":0},{"chargeId":6,"stock":0,"purchaseDate":"2025-02-17 20:01:42","validDate":"2025-02-17 20:01:42","extNum1":0},{"chargeId":7,"stock":0,"purchaseDate":"2025-02-06 16:17:41","validDate":"2025-02-06 16:17:41","extNum1":0},{"chargeId":8,"stock":0,"purchaseDate":"2025-02-06 16:17:49","validDate":"2025-02-06 16:17:49","extNum1":0},{"chargeId":9,"stock":0,"purchaseDate":"2025-02-06 16:18:00","validDate":"2025-02-06 16:18:00","extNum1":0},{"chargeId":10001,"stock":1,"purchaseDate":"2025-06-11 17:19:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":10005,"stock":0,"purchaseDate":"2025-04-25 15:45:55","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10105,"stock":0,"purchaseDate":"2025-04-25 15:46:00","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10205,"stock":0,"purchaseDate":"2025-04-25 15:46:03","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":11001,"stock":0,"purchaseDate":"2025-01-08 20:43:05","validDate":"2025-04-08 04:00:00","extNum1":0},{"chargeId":30001,"stock":0,"purchaseDate":"2025-04-25 15:46:17","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":999999,"stock":0,"purchaseDate":"2025-02-06 23:03:14","validDate":"2025-02-06 23:03:14","extNum1":0}]}
|
||||||
|
|
||||||
|
musicData = generateMusicData()
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
# "userData": [{
|
||||||
|
# "lastRomVersion": romVersion,
|
||||||
|
# "lastDataVersion": dataVersion
|
||||||
|
# }],
|
||||||
|
"userChargeList": currentUserChargeList,
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1", # 1避免覆盖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = testUid8
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(implWipeTickets(userId, currentLoginTimestamp, loginResult))
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
156
backend/ActionLoginBonus.py
Normal file
156
backend/ActionLoginBonus.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# ログインボーナス!やったね!
|
||||||
|
# セガ秘 内部使用のみ(トレードマーク)
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from Config import (
|
||||||
|
loginBonusDBPath,
|
||||||
|
loginBonusDBPathFallback,
|
||||||
|
)
|
||||||
|
from MyConfig import testUid
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperFullPlay import implFullPlayAction, generateMusicData
|
||||||
|
|
||||||
|
|
||||||
|
class NoSelectedBonusError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def apiQueryLoginBonus(userId: int) -> str:
|
||||||
|
"""ログインボーナスを取得する API"""
|
||||||
|
data = json.dumps({"userId": int(userId), "nextIndex": 0, "maxCount": 2000})
|
||||||
|
return apiSDGB(data, "GetUserLoginBonusApi", userId)
|
||||||
|
|
||||||
|
|
||||||
|
def implLoginBonus(
|
||||||
|
userId: int, currentLoginTimestamp: int, currentLoginResult, bonusGenerateMode=1
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
ログインボーナスデータをアップロードする
|
||||||
|
bonusGenerateMode は、ログインボーナスを生成する方法を指定します。
|
||||||
|
1: 選択したボーナスのみ MAX にする(選択したボーナスはないの場合は False を返す)
|
||||||
|
2: 全部 MAX にする
|
||||||
|
"""
|
||||||
|
musicData = generateMusicData()
|
||||||
|
# サーバーからログインボーナスデータを取得
|
||||||
|
data = json.dumps({"userId": int(userId), "nextIndex": 0, "maxCount": 2000})
|
||||||
|
UserLoginBonusResponse = json.loads(apiSDGB(data, "GetUserLoginBonusApi", userId))
|
||||||
|
# ログインボーナスリストを生成、それから処理してアップロード
|
||||||
|
UserLoginBonusList = UserLoginBonusResponse["userLoginBonusList"]
|
||||||
|
finalBonusList = generateLoginBonusList(UserLoginBonusList, bonusGenerateMode)
|
||||||
|
if not finalBonusList:
|
||||||
|
return False # ログインボーナスを選択していないから失敗
|
||||||
|
# UserAllのパッチ
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1", # 上書きしない
|
||||||
|
"userLoginBonusList": finalBonusList,
|
||||||
|
"isNewLoginBonusList": "0" * len(finalBonusList),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generateLoginBonusList(UserLoginBonusList, generateMode=1):
|
||||||
|
"""
|
||||||
|
ログインボーナスリストを生成します。
|
||||||
|
generateMode は、ログインボーナスを生成する方法を指定します。
|
||||||
|
1: 選択したボーナスのみ MAX にする(選択したボーナスはないの場合は False を返す)
|
||||||
|
2: 全部 MAX にする
|
||||||
|
"""
|
||||||
|
# HDDから、ログインボーナスデータを読み込む
|
||||||
|
# アップデートがある場合、このファイルを更新する必要があります
|
||||||
|
# 必ず最新のデータを使用してください
|
||||||
|
try:
|
||||||
|
tree = ET.parse(loginBonusDBPath)
|
||||||
|
root = tree.getroot()
|
||||||
|
loginBonusIdList = [
|
||||||
|
int(item.find("id").text) for item in root.findall(".//StringID")
|
||||||
|
]
|
||||||
|
logger.debug(f"ログインボーナスIDリスト: {loginBonusIdList}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
try:
|
||||||
|
tree = ET.parse(loginBonusDBPathFallback)
|
||||||
|
root = tree.getroot()
|
||||||
|
loginBonusIdList = [
|
||||||
|
int(item.find("id").text) for item in root.findall(".//StringID")
|
||||||
|
]
|
||||||
|
logger.debug(f"ログインボーナスIDリスト: {loginBonusIdList}")
|
||||||
|
except Exception:
|
||||||
|
raise FileNotFoundError("ログインボーナスデータベースを読み込めません")
|
||||||
|
|
||||||
|
# ログインボーナスの MAX POINT は5の場合があります
|
||||||
|
# その全部のボーナスIDをこのリストに追加してください
|
||||||
|
# 必ず最新のデータを使用してください
|
||||||
|
Bonus5Id = [12, 29, 30, 38, 43, 604, 611]
|
||||||
|
|
||||||
|
# UserBonusList から bonusId を取得
|
||||||
|
UserLoginBonusIdList = [item["bonusId"] for item in UserLoginBonusList]
|
||||||
|
|
||||||
|
# 存在しないボーナス
|
||||||
|
NonExistingBonuses = list(set(loginBonusIdList) - set(UserLoginBonusIdList))
|
||||||
|
logger.debug(f"存在しないボーナス: {NonExistingBonuses}")
|
||||||
|
|
||||||
|
bonusList = []
|
||||||
|
if generateMode == 1: # 選択したボーナスのみ MAX にする
|
||||||
|
for item in UserLoginBonusList:
|
||||||
|
if item["isCurrent"] and not item["isComplete"]:
|
||||||
|
point = 4 if item["bonusId"] in Bonus5Id else 9
|
||||||
|
data = {
|
||||||
|
"bonusId": item["bonusId"],
|
||||||
|
"point": point,
|
||||||
|
"isCurrent": True,
|
||||||
|
"isComplete": False,
|
||||||
|
}
|
||||||
|
bonusList.append(data)
|
||||||
|
if len(bonusList) == 0:
|
||||||
|
raise NoSelectedBonusError("選択したログインボーナスがありません")
|
||||||
|
elif generateMode == 2: # 全部 MAX にする
|
||||||
|
# 存在しているボーナスを追加
|
||||||
|
for item in UserLoginBonusList:
|
||||||
|
if not item["isComplete"]:
|
||||||
|
data = {
|
||||||
|
"bonusId": item["bonusId"],
|
||||||
|
"point": 4 if item["bonusId"] in Bonus5Id else 9,
|
||||||
|
"isCurrent": item["isCurrent"],
|
||||||
|
"isComplete": False,
|
||||||
|
}
|
||||||
|
bonusList.append(data)
|
||||||
|
elif item["bonusId"] == 999:
|
||||||
|
data = {
|
||||||
|
"bonusId": 999,
|
||||||
|
"point": (item["point"] // 10) * 10 + 9,
|
||||||
|
"isCurrent": item["isCurrent"],
|
||||||
|
"isComplete": False,
|
||||||
|
}
|
||||||
|
bonusList.append(data)
|
||||||
|
# 存在しないボーナスを追加
|
||||||
|
for bonusId in NonExistingBonuses:
|
||||||
|
data = {
|
||||||
|
"bonusId": bonusId,
|
||||||
|
"point": 4 if bonusId in Bonus5Id else 9,
|
||||||
|
"isCurrent": False,
|
||||||
|
"isComplete": False,
|
||||||
|
}
|
||||||
|
bonusList.append(data)
|
||||||
|
else:
|
||||||
|
raise SyntaxError("generateMode は 1 または 2 でなければなりません")
|
||||||
|
|
||||||
|
logger.debug(f"ログインボーナスリスト: {bonusList}")
|
||||||
|
return bonusList
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# ログインボーナスデータをアップロードする
|
||||||
|
userId = testUid
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
currentLoginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
implLoginBonus(userId, currentLoginTimestamp, currentLoginResult, 2)
|
||||||
|
apiLogout(currentLoginTimestamp, userId)
|
||||||
99
backend/ActionScoreRecord.py
Normal file
99
backend/ActionScoreRecord.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# 删除和上传成绩
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperFullPlay import implFullPlayAction
|
||||||
|
from MyConfig import testUid
|
||||||
|
|
||||||
|
|
||||||
|
def implDeleteMusicRecord(
|
||||||
|
userId: int,
|
||||||
|
currentLoginTimestamp: int,
|
||||||
|
currentLoginResult,
|
||||||
|
musicId: int,
|
||||||
|
levelId: int,
|
||||||
|
) -> str:
|
||||||
|
musicData = {
|
||||||
|
"musicId": musicId,
|
||||||
|
"level": levelId,
|
||||||
|
"playCount": 1,
|
||||||
|
"achievement": 0,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"deluxscoreMax": 0,
|
||||||
|
"scoreRank": 0,
|
||||||
|
"extNum1": 0,
|
||||||
|
}
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "0", # 0为编辑,即可删除掉成绩
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def implUploadMusicRecord(
|
||||||
|
userId: int,
|
||||||
|
currentLoginTimestamp: int,
|
||||||
|
currentLoginResult,
|
||||||
|
musicId: int,
|
||||||
|
levelId: int,
|
||||||
|
achievement: int,
|
||||||
|
dxScore: int,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
VERY EARLY STAGE OF UPLOADING SCORES!!!! DO NOT USE THIS!!!!
|
||||||
|
上传成绩的参考实现。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 要上传的数据
|
||||||
|
musicData = {
|
||||||
|
"musicId": musicId,
|
||||||
|
"level": levelId,
|
||||||
|
"playCount": 1,
|
||||||
|
"achievement": achievement,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"deluxscoreMax": dxScore,
|
||||||
|
"scoreRank": 0,
|
||||||
|
"extNum1": 0,
|
||||||
|
}
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1", # 0编辑 1插入
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = testUid
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
musicId = 852 # 852 is tiamat
|
||||||
|
levelId = 3 # 3 is MASTER
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
implDeleteMusicRecord(
|
||||||
|
userId, currentLoginTimestamp, loginResult, musicId, levelId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# logger.info(implUploadMusicRecord(userId, currentLoginTimestamp, loginResult, musicId, levelId, 1000000, 100))
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
71
backend/ActionUnlockItem.py
Normal file
71
backend/ActionUnlockItem.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# 解锁一些东西的外部代码
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from MyConfig import testUid8
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperUnlockThing import implUnlockThing
|
||||||
|
|
||||||
|
|
||||||
|
def implUnlockMultiItem(
|
||||||
|
itemKind: int,
|
||||||
|
userId: int,
|
||||||
|
currentLoginTimestamp: int,
|
||||||
|
currentLoginResult,
|
||||||
|
*itemIds: int,
|
||||||
|
) -> str:
|
||||||
|
if not itemIds:
|
||||||
|
logger.info("无操作,跳过处理!")
|
||||||
|
return
|
||||||
|
"""
|
||||||
|
发单个东西,比如搭档 10
|
||||||
|
"""
|
||||||
|
userItemList = [
|
||||||
|
{"itemKind": itemKind, "itemId": itemId, "stock": 1, "isValid": True}
|
||||||
|
for itemId in itemIds
|
||||||
|
]
|
||||||
|
unlockThingResult = implUnlockThing(
|
||||||
|
userItemList, userId, currentLoginTimestamp, currentLoginResult
|
||||||
|
)
|
||||||
|
return unlockThingResult
|
||||||
|
|
||||||
|
|
||||||
|
def implUnlockMusic(
|
||||||
|
musicToBeUnlocked: int, userId: int, currentLoginTimestamp: int, currentLoginResult
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
解锁乐曲
|
||||||
|
"""
|
||||||
|
userItemList = [
|
||||||
|
{"itemKind": 5, "itemId": musicToBeUnlocked, "stock": 1, "isValid": True},
|
||||||
|
{"itemKind": 6, "itemId": musicToBeUnlocked, "stock": 1, "isValid": True},
|
||||||
|
]
|
||||||
|
unlockThingResult = implUnlockThing(
|
||||||
|
userItemList, userId, currentLoginTimestamp, currentLoginResult
|
||||||
|
)
|
||||||
|
return unlockThingResult
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = int(input("type user id: ").strip() or "0") or testUid8
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
items = range(11, 33) # all partners
|
||||||
|
logger.info(
|
||||||
|
implUnlockMultiItem(
|
||||||
|
10,
|
||||||
|
userId,
|
||||||
|
currentLoginTimestamp,
|
||||||
|
loginResult,
|
||||||
|
*items,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
199
backend/Best50_To_Diving_Fish.py
Normal file
199
backend/Best50_To_Diving_Fish.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperGetUserMusicDetail import getUserFullMusicDetail
|
||||||
|
from HelperMusicDB import getMusicTitle
|
||||||
|
|
||||||
|
|
||||||
|
class divingFishAuthFailError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class divingFishCommError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# 水鱼查分器的 API 地址
|
||||||
|
BASE_URL = "https://www.diving-fish.com/api/maimaidxprober"
|
||||||
|
|
||||||
|
# 水鱼查分器的成绩状态转换
|
||||||
|
COMBO_ID_TO_NAME = ["", "fc", "fcp", "ap", "app"]
|
||||||
|
SYNC_ID_TO_NAME = ["", "fs", "fsp", "fsd", "fsdp", "sync"]
|
||||||
|
|
||||||
|
|
||||||
|
def apiDivingFish(method: str, apiPath: str, importToken: str, data=None):
|
||||||
|
"""水鱼查分器的 API 通讯实现"""
|
||||||
|
headers = {"Import-Token": importToken}
|
||||||
|
if method == "POST":
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
logger.info(f"水鱼查分器 API 请求:{method} {BASE_URL + apiPath}")
|
||||||
|
if method == "POST":
|
||||||
|
response = requests.post(
|
||||||
|
url=BASE_URL + apiPath,
|
||||||
|
json=data,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
elif method == "GET":
|
||||||
|
response = requests.get(
|
||||||
|
url=BASE_URL + apiPath,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
elif method == "DELETE":
|
||||||
|
response = requests.delete(
|
||||||
|
url=BASE_URL + apiPath,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"未知的请求方法:{method}")
|
||||||
|
raise ValueError(f"未知的请求方法:{method}")
|
||||||
|
|
||||||
|
logger.info(f"水鱼查分器请求结果:{response.status_code}")
|
||||||
|
logger.debug(f"水鱼查分器回应:{response.text}")
|
||||||
|
finalResponseTextDecode = response.text.encode("utf-8").decode("unicode_escape")
|
||||||
|
logger.debug(f"水鱼查分器回应解码后:{finalResponseTextDecode}")
|
||||||
|
match response.status_code:
|
||||||
|
case 200:
|
||||||
|
return response.json()
|
||||||
|
case 500:
|
||||||
|
raise divingFishAuthFailError
|
||||||
|
case _:
|
||||||
|
raise divingFishCommError
|
||||||
|
|
||||||
|
|
||||||
|
def getFishRecords(importToken: str) -> dict:
|
||||||
|
"""获取水鱼查分器的成绩"""
|
||||||
|
return apiDivingFish("GET", "/player/records", importToken)
|
||||||
|
|
||||||
|
|
||||||
|
def updateFishRecords(importToken: str, records: list[dict]) -> dict:
|
||||||
|
"""上传成绩到水鱼查分器"""
|
||||||
|
return apiDivingFish("POST", "/player/update_records", importToken, records)
|
||||||
|
|
||||||
|
|
||||||
|
def resetFishRecords(fishImportToken: str):
|
||||||
|
"""重置水鱼查分器的用户数据"""
|
||||||
|
return apiDivingFish("DELETE", "/player/delete_records", fishImportToken)
|
||||||
|
|
||||||
|
|
||||||
|
def getFishUserInfo(userQQ: int):
|
||||||
|
"""按QQ获取水鱼查分器的用户信息"""
|
||||||
|
return apiDivingFish("POST", "/query/player", "", {"qq": userQQ})
|
||||||
|
|
||||||
|
|
||||||
|
def maimaiUserMusicDetailToDivingFishFormat(userMusicDetailList) -> list:
|
||||||
|
"""舞萌的 UserMusicDetail 成绩格式转换成水鱼的格式"""
|
||||||
|
divingFishList = []
|
||||||
|
for currentMusicDetail in userMusicDetailList:
|
||||||
|
# musicId 大于 100000 属于宴谱,不计入
|
||||||
|
if currentMusicDetail["musicId"] >= 100000:
|
||||||
|
continue
|
||||||
|
# 获得歌名
|
||||||
|
currentMusicTitle = getMusicTitle(currentMusicDetail["musicId"])
|
||||||
|
# 如果数据库里未找到此歌曲
|
||||||
|
if currentMusicTitle == "R_ERR_MUSIC_ID_NOT_IN_DATABASE":
|
||||||
|
logger.warning(f"数据库无此歌曲 跳过: {currentMusicDetail['musicId']}")
|
||||||
|
continue
|
||||||
|
# 每一个乐曲都判断下是 DX 还是标准
|
||||||
|
if currentMusicDetail["musicId"] >= 10000:
|
||||||
|
notesType = "DX"
|
||||||
|
else:
|
||||||
|
notesType = "SD"
|
||||||
|
# 追加进列表
|
||||||
|
try:
|
||||||
|
divingFishList.append(
|
||||||
|
{
|
||||||
|
"achievements": (
|
||||||
|
currentMusicDetail["achievement"] / 10000
|
||||||
|
), # 水鱼的成绩是 float 而非舞萌的 int
|
||||||
|
"title": currentMusicTitle,
|
||||||
|
"type": notesType,
|
||||||
|
"level_index": currentMusicDetail["level"],
|
||||||
|
"fc": COMBO_ID_TO_NAME[currentMusicDetail["comboStatus"]],
|
||||||
|
"fs": SYNC_ID_TO_NAME[currentMusicDetail["syncStatus"]],
|
||||||
|
"dxScore": currentMusicDetail["deluxscoreMax"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.error(f"无法将 UserMusic 翻译成水鱼格式: {currentMusicDetail}")
|
||||||
|
|
||||||
|
return divingFishList
|
||||||
|
|
||||||
|
|
||||||
|
def isVaildFishToken(importToken: str):
|
||||||
|
"""通过尝试获取一次成绩,检查水鱼查分器的 Token 是否有效
|
||||||
|
有效返回 True,无效返回 False"""
|
||||||
|
result = apiDivingFish("GET", "/player/records", importToken)
|
||||||
|
logger.debug(f"水鱼查分器 Token 检查结果:{result}")
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def implGetUserCurrentDXRating(userQQ: int):
|
||||||
|
"""获取用户当前的 DX RATING"""
|
||||||
|
try:
|
||||||
|
playerData = getFishUserInfo(userQQ)
|
||||||
|
playerRating = playerData["rating"]
|
||||||
|
logger.info(f"用户 {userQQ} 的 DX RATING 是 {playerRating}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法获取用户 {userQQ} 的 DX RATING: {e}")
|
||||||
|
return False
|
||||||
|
return playerRating
|
||||||
|
|
||||||
|
|
||||||
|
def implUserMusicToDivingFish(userId: int, fishImportToken: str):
|
||||||
|
"""上传所有成绩到水鱼的参考实现。
|
||||||
|
返回一个 int 的 ErrorCode。
|
||||||
|
0: Success
|
||||||
|
1: Get User Music Fail
|
||||||
|
2: Auth Fail
|
||||||
|
3: Comm Error
|
||||||
|
"""
|
||||||
|
logger.info("开始尝试上传舞萌成绩到水鱼查分器!")
|
||||||
|
try:
|
||||||
|
userFullMusicDetailList = getUserFullMusicDetail(userId)
|
||||||
|
logger.info("成功得到成绩!转换成水鱼格式..")
|
||||||
|
divingFishData = maimaiUserMusicDetailToDivingFishFormat(
|
||||||
|
userFullMusicDetailList
|
||||||
|
)
|
||||||
|
logger.info("转换成功!开始上传水鱼..")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取成绩失败!{e}")
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
updateFishRecords(fishImportToken, divingFishData)
|
||||||
|
except divingFishAuthFailError:
|
||||||
|
logger.error("水鱼查分器认证失败!")
|
||||||
|
return 2
|
||||||
|
except divingFishCommError:
|
||||||
|
logger.error("水鱼查分器通讯失败!")
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
|
def generateDebugTestScore():
|
||||||
|
"""生成测试成绩"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"achievement": 1010000,
|
||||||
|
"comboStatus": 4,
|
||||||
|
"deluxscoreMax": 4026,
|
||||||
|
"level": 4,
|
||||||
|
"musicId": 834,
|
||||||
|
"syncStatus": 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement": 1010000,
|
||||||
|
"comboStatus": 4,
|
||||||
|
"deluxscoreMax": 4200,
|
||||||
|
"level": 4,
|
||||||
|
"musicId": 11663,
|
||||||
|
"syncStatus": 4,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = int(input("userId: "))
|
||||||
|
fishImportToken = input("DivingFish Token: ")
|
||||||
|
|
||||||
|
implUserMusicToDivingFish(userId, fishImportToken)
|
||||||
126
backend/ChargeTicket.py
Normal file
126
backend/ChargeTicket.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# 倍票相关 API 的实现
|
||||||
|
import rapidjson as json
|
||||||
|
import pytz
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from HelperGetUserThing import implGetUser_
|
||||||
|
|
||||||
|
from Config import (
|
||||||
|
clientId,
|
||||||
|
placeId,
|
||||||
|
regionId,
|
||||||
|
)
|
||||||
|
from MyConfig import testUid2
|
||||||
|
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperFullPlay import implFullPlayAction, generateMusicData
|
||||||
|
|
||||||
|
|
||||||
|
def implWipeTickets(userId: int, currentLoginTimestamp: int, currentLoginResult) -> str:
|
||||||
|
"""清空用户所有票的 API 请求器,返回 Json String。"""
|
||||||
|
# 先得到当前用户的 Charge 数据
|
||||||
|
currentUserCharge = implGetUser_("Charge", userId)
|
||||||
|
# 取得 List
|
||||||
|
currentUserChargeList = currentUserCharge["userChargeList"]
|
||||||
|
# 所有 stock 都置为 0
|
||||||
|
for charge in currentUserChargeList:
|
||||||
|
charge["stock"] = 0
|
||||||
|
|
||||||
|
musicData = generateMusicData()
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userChargeList": currentUserChargeList,
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1", # 1避免覆盖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def apiQueryTicket(userId: int) -> str:
|
||||||
|
"""查询已有票的 API 请求器,返回 Json String。"""
|
||||||
|
# 构建 Payload
|
||||||
|
data = json.dumps({"userId": userId})
|
||||||
|
# 发送请求
|
||||||
|
userdata_result = apiSDGB(data, "GetUserChargeApi", userId)
|
||||||
|
# 返回响应
|
||||||
|
return userdata_result
|
||||||
|
|
||||||
|
|
||||||
|
def apiBuyTicket(
|
||||||
|
userId: int, ticketType: int, price: int, playerRating: int, playCount: int
|
||||||
|
) -> str:
|
||||||
|
"""倍票购买 API 的请求器"""
|
||||||
|
|
||||||
|
nowTime = datetime.now(pytz.timezone("Asia/Shanghai"))
|
||||||
|
|
||||||
|
# 构造请求数据 Payload
|
||||||
|
data = json.dumps(
|
||||||
|
{
|
||||||
|
"userId": userId,
|
||||||
|
"userChargelog": {
|
||||||
|
"chargeId": ticketType,
|
||||||
|
"price": price,
|
||||||
|
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
|
||||||
|
"playCount": playCount,
|
||||||
|
"playerRating": playerRating,
|
||||||
|
"placeId": placeId,
|
||||||
|
"regionId": regionId,
|
||||||
|
"clientId": clientId,
|
||||||
|
},
|
||||||
|
"userCharge": {
|
||||||
|
"chargeId": ticketType,
|
||||||
|
"stock": 1,
|
||||||
|
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
|
||||||
|
"validDate": (nowTime + timedelta(days=90))
|
||||||
|
.replace(hour=4, minute=0, second=0)
|
||||||
|
.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 发送请求,返回最终得到的 Json String 回执
|
||||||
|
return apiSDGB(data, "UpsertUserChargelogApi", userId)
|
||||||
|
|
||||||
|
|
||||||
|
def implBuyTicket(userId: int, ticketType: int):
|
||||||
|
"""
|
||||||
|
购买倍票 API 的参考实现。
|
||||||
|
需要事先登录.
|
||||||
|
返回服务器响应的 Json string。
|
||||||
|
"""
|
||||||
|
# 先使用 GetUserData API 请求器,取得 rating 和 pc 数
|
||||||
|
currentUserData = implGetUser_("Data", userId)
|
||||||
|
if currentUserData:
|
||||||
|
playerRating = currentUserData["userData"]["playerRating"]
|
||||||
|
playCount = currentUserData["userData"].get("playCount", 0)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
# 正式买票
|
||||||
|
getTicketResponseStr = apiBuyTicket(
|
||||||
|
userId, ticketType, ticketType - 1, playerRating, playCount
|
||||||
|
)
|
||||||
|
# 返回结果
|
||||||
|
return getTicketResponseStr
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = testUid2
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(implBuyTicket(userId, 2)) # 购买倍票
|
||||||
|
# logger.info(apiQueryTicket(userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
16
backend/Config.py
Normal file
16
backend/Config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
regionId = 13
|
||||||
|
regionName = "河南"
|
||||||
|
placeId = 2411
|
||||||
|
placeName = "智游星期六河南郑州店"
|
||||||
|
clientId = "A63E01E6154"
|
||||||
|
|
||||||
|
useProxy = False
|
||||||
|
proxyUrl = "http://100.104.133.113:33080"
|
||||||
|
|
||||||
|
loginBonusDBPath = "./Data/loginBonusDB.xml"
|
||||||
|
musicDBPath = "./Data/musicDB.json"
|
||||||
|
|
||||||
|
loginBonusDBPathFallback = "./maimaiDX-Api/Data/loginBonusDB.xml"
|
||||||
|
musicDBPathFallback = "./maimaiDX-Api/Data/musicDB.json"
|
||||||
|
|
||||||
|
# 日本精工,安全防漏
|
||||||
318
backend/Data/loginBonusDB.xml
Normal file
318
backend/Data/loginBonusDB.xml
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<SerializeSortData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<dataName>loginBonus</dataName>
|
||||||
|
<SortList>
|
||||||
|
<StringID>
|
||||||
|
<id>53</id>
|
||||||
|
<str>パートナー:ソルト(ぷりずむ)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>48</id>
|
||||||
|
<str>パートナー:ラズ(ばでぃーず)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>38</id>
|
||||||
|
<str>パートナー:ずんだもん</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>39</id>
|
||||||
|
<str>パートナー:乙姫(ばでぃーず)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>40</id>
|
||||||
|
<str>パートナー:らいむっくま&れもんっくま(ばでぃーず)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>34</id>
|
||||||
|
<str>パートナー:黒姫</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>24</id>
|
||||||
|
<str>パートナー:ラズ(ふぇすてぃばる)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>25</id>
|
||||||
|
<str>パートナー:シフォン(ふぇすてぃばる)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>26</id>
|
||||||
|
<str>パートナー:ソルト(ふぇすてぃばる)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>19</id>
|
||||||
|
<str>パートナー:ちびみるく</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>20</id>
|
||||||
|
<str>パートナー:百合咲ミカ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>8</id>
|
||||||
|
<str>パートナー:しゃま(ゆにばーす)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>9</id>
|
||||||
|
<str>パートナー:みるく(ゆにばーす)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>7</id>
|
||||||
|
<str>パートナー:乙姫(すぷらっしゅ)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>1</id>
|
||||||
|
<str>パートナー:乙姫</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>2</id>
|
||||||
|
<str>パートナー:ラズ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>3</id>
|
||||||
|
<str>パートナー:シフォン</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>4</id>
|
||||||
|
<str>パートナー:ソルト</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>5</id>
|
||||||
|
<str>パートナー:しゃま</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>6</id>
|
||||||
|
<str>パートナー:みるく</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>54</id>
|
||||||
|
<str>でらっくす譜面:One Step Ahead</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>55</id>
|
||||||
|
<str>でらっくす譜面:天狗の落とし文 feat. ytr</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>612</id>
|
||||||
|
<str>でらっくす譜面:LANCE</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>613</id>
|
||||||
|
<str>でらっくす譜面:HYP3RTRIBE</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>614</id>
|
||||||
|
<str>でらっくす譜面:Imitation:Loud Lounge</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>49</id>
|
||||||
|
<str>でらっくす譜面:ハッピーシンセサイザ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>50</id>
|
||||||
|
<str>でらっくす譜面:sweet little sister</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>608</id>
|
||||||
|
<str>でらっくす譜面:御旗のもとに</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>609</id>
|
||||||
|
<str>でらっくす譜面:炎歌 -ほむらうた-</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>610</id>
|
||||||
|
<str>でらっくす譜面:華の集落、秋のお届け</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>605</id>
|
||||||
|
<str>でらっくす譜面:oboro</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>606</id>
|
||||||
|
<str>でらっくす譜面:ナミダと流星</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>607</id>
|
||||||
|
<str>スタンダード譜面:渦状銀河のシンフォニエッタ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>508</id>
|
||||||
|
<str>でらっくす譜面:Latent Kingdom</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>41</id>
|
||||||
|
<str>でらっくす譜面:初音ミクの消失</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>42</id>
|
||||||
|
<str>でらっくす譜面:色は匂へど散りぬるを</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>601</id>
|
||||||
|
<str>でらっくす譜面:BULK UP (GAME EXCLUSIVE EDIT)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>602</id>
|
||||||
|
<str>でらっくす譜面:Monochrome Rainbow</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>603</id>
|
||||||
|
<str>でらっくす譜面:Selector</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>35</id>
|
||||||
|
<str>でらっくす譜面:深海少女</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>36</id>
|
||||||
|
<str>でらっくす譜面:ナイト・オブ・ナイツ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>27</id>
|
||||||
|
<str>でらっくす譜面:M.S.S.Planet</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>28</id>
|
||||||
|
<str>でらっくす譜面:響縁</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>501</id>
|
||||||
|
<str>スタンダード譜面:Halcyon</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>502</id>
|
||||||
|
<str>スタンダード譜面:サンバランド</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>503</id>
|
||||||
|
<str>でらっくす譜面:Starlight Disco</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>504</id>
|
||||||
|
<str>でらっくす譜面:火炎地獄</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>505</id>
|
||||||
|
<str>スタンダード譜面:VIIIbit Explorer</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>506</id>
|
||||||
|
<str>でらっくす譜面:Maxi</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>507</id>
|
||||||
|
<str>でらっくす譜面:ケロ⑨destiny</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>21</id>
|
||||||
|
<str>でらっくす譜面:セツナトリップ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>22</id>
|
||||||
|
<str>でらっくす譜面:Grip & Break down !!</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>17</id>
|
||||||
|
<str>でらっくす譜面:ゴーストルール</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>18</id>
|
||||||
|
<str>でらっくす譜面:taboo tears you up</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>56</id>
|
||||||
|
<str>アイコン:PRiSM</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>611</id>
|
||||||
|
<str>アイコン:BUDDiES 乙姫&ラズ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>43</id>
|
||||||
|
<str>アイコン:BUDDiES</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>604</id>
|
||||||
|
<str>アイコン:FESTiVAL ラズ&シフォン&ソルト</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>29</id>
|
||||||
|
<str>アイコン:FESTiVAL</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>30</id>
|
||||||
|
<str>アイコン:Lia=Fail</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>12</id>
|
||||||
|
<str>アイコン:UNiVERSE</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>57</id>
|
||||||
|
<str>ネームプレート:QuiQ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>51</id>
|
||||||
|
<str>ネームプレート:Swift Swing</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>14</id>
|
||||||
|
<str>ネームプレート:はっぴー(ゆにばーす)</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>58</id>
|
||||||
|
<str>フレーム:Valsqotch</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>52</id>
|
||||||
|
<str>フレーム:Latent Kingdom</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>44</id>
|
||||||
|
<str>フレーム:mystique as iris</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>45</id>
|
||||||
|
<str>フレーム:VeRForTe αRtE:VEiN</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>37</id>
|
||||||
|
<str>フレーム:Tricolor⁂circuS</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>31</id>
|
||||||
|
<str>フレーム:Heavenly Blast</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>32</id>
|
||||||
|
<str>フレーム:sølips</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>33</id>
|
||||||
|
<str>フレーム:Rainbow Rush Story</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>23</id>
|
||||||
|
<str>フレーム:ふたりでばかんすにゃ♪</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>15</id>
|
||||||
|
<str>フレーム:ここからはじまるプロローグ。</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>16</id>
|
||||||
|
<str>フレーム:モ゜ルモ゜ル</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>10</id>
|
||||||
|
<str>フレーム:黒姫</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>11</id>
|
||||||
|
<str>フレーム:百合咲ミカ</str>
|
||||||
|
</StringID>
|
||||||
|
<StringID>
|
||||||
|
<id>999</id>
|
||||||
|
<str>ちほー進行1.5倍チケット</str>
|
||||||
|
</StringID>
|
||||||
|
</SortList>
|
||||||
|
</SerializeSortData>
|
||||||
1597
backend/Data/musicDB.json
Normal file
1597
backend/Data/musicDB.json
Normal file
File diff suppressed because it is too large
Load Diff
107
backend/ForceLogout.py
Normal file
107
backend/ForceLogout.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# 解小黑屋实现
|
||||||
|
# 仍十分不完善,不建议使用
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
|
||||||
|
from GetPreview import apiGetUserPreview
|
||||||
|
from HelperLogInOut import apiLogout
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from MyConfig import testUid2
|
||||||
|
|
||||||
|
|
||||||
|
def isUserLoggedIn(userId):
|
||||||
|
isLogin = json.loads(apiGetUserPreview(userId, True))["isLogin"]
|
||||||
|
logger.debug(f"用户 {userId} 是否登录: {isLogin}")
|
||||||
|
return isLogin
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableTime(unixTime):
|
||||||
|
"""将 Unix 时间戳转换为人类可读的时间"""
|
||||||
|
# 减一个小时,因为舞萌貌似是 UTC+9
|
||||||
|
timestamp = int(unixTime) - 3600
|
||||||
|
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def getMaimaiUNIXTime(mmddhhmmss, year=2025):
|
||||||
|
"""
|
||||||
|
解析用户输入的 MMDDHHMMSS 格式的时间,返回 Unix 时间戳
|
||||||
|
时间会被推后一个小时,因为舞萌貌似是 UTC+9?
|
||||||
|
"""
|
||||||
|
# date_time_str = f"{year}{mmddhhmmss}"
|
||||||
|
# 加上一个小时
|
||||||
|
date_time_str = (
|
||||||
|
f"{year}{mmddhhmmss[:4]}{int(mmddhhmmss[4:6]) + 1:02}{mmddhhmmss[6:]}"
|
||||||
|
)
|
||||||
|
date_time_obj = datetime.strptime(date_time_str, "%Y%m%d%H%M%S")
|
||||||
|
# 将 datetime 对象转换为 Unix 时间戳
|
||||||
|
unix_timestamp = int(time.mktime(date_time_obj.timetuple()))
|
||||||
|
logger.info(f"转换出了时间戳: {unix_timestamp}")
|
||||||
|
return unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def logOut(userId, Timestamp):
|
||||||
|
"""极其简单的登出实现,成功返回 True,失败返回 False
|
||||||
|
注意:不会检查用户是否真的登出了,只会尝试登出"""
|
||||||
|
try:
|
||||||
|
if apiLogout(Timestamp, userId, True)["returnCode"] == 1:
|
||||||
|
# 成功送出了登出请求
|
||||||
|
logger.debug(f"已成功尝试登出用户 {userId}")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logger.error(f"登出用户 {userId} 的时候发生了错误")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def isCorrectTimestamp(timestamp, userId):
|
||||||
|
"""
|
||||||
|
动作:给定一个时间戳,用它尝试登出用户,然后检查用户是否成功登出。
|
||||||
|
如果用户成功登出,返回 True;否则返回 False。
|
||||||
|
"""
|
||||||
|
if not logOut(userId, timestamp):
|
||||||
|
logger.error(f"用时间戳 {timestamp} 登出用户 {userId} 的时候发生了错误")
|
||||||
|
return False
|
||||||
|
isLoggedOut = not isUserLoggedIn(userId)
|
||||||
|
logger.debug(f"时间戳 {timestamp} 是否正确: {isLoggedOut}")
|
||||||
|
return isLoggedOut
|
||||||
|
|
||||||
|
|
||||||
|
def findCorrectTimestamp(timestamp, userId, max_attempts=600):
|
||||||
|
# 初始化偏移量
|
||||||
|
offset = 1
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
while attempts < max_attempts:
|
||||||
|
# 尝试当前时间戳
|
||||||
|
currentTryTimestamp = timestamp
|
||||||
|
if isCorrectTimestamp(currentTryTimestamp, userId):
|
||||||
|
logger.info(f"正确的时间戳: {currentTryTimestamp}")
|
||||||
|
return currentTryTimestamp
|
||||||
|
|
||||||
|
# 增加偏移量尝试
|
||||||
|
currentTryTimestamp = timestamp + offset
|
||||||
|
if isCorrectTimestamp(currentTryTimestamp, userId):
|
||||||
|
logger.info(f"正确的时间戳(在给定时间以后): {currentTryTimestamp}")
|
||||||
|
return currentTryTimestamp
|
||||||
|
|
||||||
|
# 减少偏移量尝试
|
||||||
|
currentTryTimestamp = timestamp - offset
|
||||||
|
if isCorrectTimestamp(currentTryTimestamp, userId):
|
||||||
|
logger.info(f"正确的时间戳(在给定时间以前): {currentTryTimestamp}")
|
||||||
|
return currentTryTimestamp
|
||||||
|
|
||||||
|
# 增加尝试次数和偏移量
|
||||||
|
attempts += 2
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
logger.error(f"无法找到正确的时间戳,尝试次数超过了 {max_attempts}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
human_time = "0207155500"
|
||||||
|
beginTimestamp = getMaimaiUNIXTime(human_time)
|
||||||
|
print(f"我们将开始用这个时间戳开始尝试: {beginTimestamp}")
|
||||||
|
correctTimestamp = findCorrectTimestamp(beginTimestamp, testUid2)
|
||||||
31
backend/GetAny.py
Normal file
31
backend/GetAny.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
import rapidjson as json
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetAny(
|
||||||
|
userId,
|
||||||
|
apiName: str,
|
||||||
|
noLog: bool = False,
|
||||||
|
) -> str:
|
||||||
|
data = json.dumps({"userId": int(userId)})
|
||||||
|
preview_result = apiSDGB(data, apiName, userId, noLog)
|
||||||
|
return preview_result
|
||||||
|
|
||||||
|
|
||||||
|
# CLI 示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = input("请输入用户 ID:")
|
||||||
|
# userId = testUid8
|
||||||
|
# print(apiGetAny(userId, "GetUserRatingApi"))
|
||||||
|
# print(apiGetAny(userId, "GetUserPreviewApi"))
|
||||||
|
|
||||||
|
for type in ["course", "extend", "character", "activity", "charge", "option", "region"]:
|
||||||
|
try:
|
||||||
|
data = apiGetAny(userId, f"GetUser{type.title()}Api", noLog=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed when scraping {type}: {e}")
|
||||||
|
else:
|
||||||
|
print(f"{type}:", json.dumps(json.loads(data), ensure_ascii=False, indent=4))
|
||||||
18
backend/GetPreview.py
Normal file
18
backend/GetPreview.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetUserPreview(userId, noLog: bool = False) -> str:
|
||||||
|
data = json.dumps({"userId": int(userId)})
|
||||||
|
preview_result = apiSDGB(data, "GetUserPreviewApi", userId, noLog)
|
||||||
|
return preview_result
|
||||||
|
|
||||||
|
|
||||||
|
# CLI 示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = input("请输入用户 ID:")
|
||||||
|
# userId = testUid8
|
||||||
|
print(apiGetUserPreview(userId))
|
||||||
|
print(apiSDGB("{}", "Ping", userId, False))
|
||||||
21
backend/GetPreview2024.py
Normal file
21
backend/GetPreview2024.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
|
||||||
|
from API_TitleServer import apiSDGB_2024, use2024Api
|
||||||
|
|
||||||
|
use2024Api()
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetUserPreview(userId, noLog: bool = False) -> str:
|
||||||
|
data = json.dumps({"userId": int(userId)})
|
||||||
|
preview_result = apiSDGB_2024(data, "GetUserPreviewApi", userId, noLog)
|
||||||
|
return preview_result
|
||||||
|
|
||||||
|
|
||||||
|
# CLI 示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = input("请输入用户 ID:")
|
||||||
|
# userId = testUid8
|
||||||
|
# print(apiGetUserPreview(userId))
|
||||||
|
print(apiSDGB_2024("{}", "Ping", userId, False))
|
||||||
18
backend/GetRating.py
Normal file
18
backend/GetRating.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetUserPreview(userId, noLog: bool = False) -> str:
|
||||||
|
data = json.dumps({"userId": int(userId)})
|
||||||
|
preview_result = apiSDGB(data, "GetUserRatingApi", userId, noLog)
|
||||||
|
return preview_result
|
||||||
|
|
||||||
|
|
||||||
|
# CLI 示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = input("请输入用户 ID:")
|
||||||
|
# userId = testUid8
|
||||||
|
print(apiGetUserPreview(userId))
|
||||||
|
print(apiSDGB("{}", "Ping", userId, False))
|
||||||
34
backend/GetUserAll.py
Normal file
34
backend/GetUserAll.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperFullPlay import implFullPlayAction
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from MyConfig import testUid8
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = int(input("type user id: ").strip() or "0") or testUid8
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
items = [23]
|
||||||
|
musicData = {
|
||||||
|
"musicId": 11538, # Amber Chronicle
|
||||||
|
"level": 0,
|
||||||
|
"playCount": 1,
|
||||||
|
"achievement": 0,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"deluxscoreMax": 0,
|
||||||
|
"scoreRank": 0,
|
||||||
|
"extNum1": 0,
|
||||||
|
}
|
||||||
|
implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, loginResult, musicData, {}, debugMode=True
|
||||||
|
)
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
30
backend/GetUserData.py
Normal file
30
backend/GetUserData.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
import rapidjson as json
|
||||||
|
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from MyConfig import testUid8
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetUserData(userId, noLog: bool = False) -> str:
|
||||||
|
data = json.dumps({"userId": int(userId)})
|
||||||
|
preview_result = apiSDGB(data, "GetUserDataApi", userId, noLog)
|
||||||
|
return preview_result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = int(input("type user id: ").strip() or "0") or testUid8
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(apiGetUserData(userId, noLog=False))
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
43
backend/HashEntertainment/All_API_Names.txt
Normal file
43
backend/HashEntertainment/All_API_Names.txt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
GetUserCardApi
|
||||||
|
GetUserCharacterApi
|
||||||
|
GetUserChargeApi
|
||||||
|
GetUserCourseApi
|
||||||
|
GetUserDataApi
|
||||||
|
GetUserExtendApi
|
||||||
|
GetUserFavoriteApi
|
||||||
|
GetUserFriendSeasonRankingApi
|
||||||
|
GetUserGhostApi
|
||||||
|
GetUserItemApi
|
||||||
|
GetGameChargeApi
|
||||||
|
GetUserLoginBonusApi
|
||||||
|
GetGameEventApi
|
||||||
|
GetUserMapApi
|
||||||
|
GetGameNgMusicIdApi
|
||||||
|
GetUserMusicApi
|
||||||
|
GetGameRankingApi
|
||||||
|
GetGameSettingApi
|
||||||
|
GetUserOptionApi
|
||||||
|
GetUserPortraitApi
|
||||||
|
GetGameTournamentInfoApi
|
||||||
|
UserLogoutApi
|
||||||
|
GetUserPreviewApi
|
||||||
|
GetTransferFriendApi
|
||||||
|
GetUserRatingApi
|
||||||
|
GetUserActivityApi
|
||||||
|
GetUserRecommendRateMusicApi
|
||||||
|
GetUserRecommendSelectMusicApi
|
||||||
|
GetUserRegionApi
|
||||||
|
GetUserScoreRankingApi
|
||||||
|
UploadUserPhotoApi
|
||||||
|
UploadUserPlaylogApi
|
||||||
|
UploadUserPortraitApi
|
||||||
|
UpsertClientBookkeepingApi
|
||||||
|
UpsertClientSettingApi
|
||||||
|
UpsertClientTestmodeApi
|
||||||
|
UpsertClientUploadApi
|
||||||
|
UpsertUserAllApi
|
||||||
|
UpsertUserChargelogApi
|
||||||
|
UserLoginApi
|
||||||
|
Ping
|
||||||
|
GetUserFavoriteItemApi
|
||||||
|
GetGameNgWordListApi
|
||||||
132
backend/HelperFullPlay.py
Normal file
132
backend/HelperFullPlay.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from API_TitleServer import (
|
||||||
|
SDGBRequestError,
|
||||||
|
SDGBApiError,
|
||||||
|
apiSDGB,
|
||||||
|
calcPlaySpecial,
|
||||||
|
)
|
||||||
|
from HelperGetUserThing import implGetUser_
|
||||||
|
from HelperUploadUserPlayLog import apiUploadUserPlaylog
|
||||||
|
from HelperUserAll import generateFullUserAll
|
||||||
|
|
||||||
|
|
||||||
|
def generateMusicData():
|
||||||
|
"""生成一份占位的音乐数据"""
|
||||||
|
return {
|
||||||
|
"musicId": 834, # PANDORA PARADOXXX
|
||||||
|
"level": 4,
|
||||||
|
"playCount": 1,
|
||||||
|
"achievement": 0,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"deluxscoreMax": 0,
|
||||||
|
"scoreRank": 0,
|
||||||
|
"extNum1": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def applyUserAllPatches(userAll, patches):
|
||||||
|
"""
|
||||||
|
递归地将给定的补丁应用到用户数据的各个层次。
|
||||||
|
|
||||||
|
:param userAll: 原始用户数据
|
||||||
|
:param patches: 包含所有patch的字典
|
||||||
|
"""
|
||||||
|
for key, value in patches.items():
|
||||||
|
if (
|
||||||
|
isinstance(value, dict)
|
||||||
|
and key in userAll
|
||||||
|
and isinstance(userAll[key], dict)
|
||||||
|
):
|
||||||
|
# 如果patch的值是字典,并且userAll中对应的key也是字典,递归处理
|
||||||
|
applyUserAllPatches(userAll[key], value)
|
||||||
|
elif (
|
||||||
|
isinstance(value, list)
|
||||||
|
and key in userAll
|
||||||
|
and isinstance(userAll[key], list)
|
||||||
|
):
|
||||||
|
# 如果值是列表,进行详细的更新处理
|
||||||
|
for i, patch_item in enumerate(value):
|
||||||
|
if (
|
||||||
|
i < len(userAll[key])
|
||||||
|
and isinstance(patch_item, dict)
|
||||||
|
and isinstance(userAll[key][i], dict)
|
||||||
|
):
|
||||||
|
# 如果列表项是字典,更新字典中的字段
|
||||||
|
applyUserAllPatches(userAll[key][i], patch_item)
|
||||||
|
elif i >= len(userAll[key]):
|
||||||
|
# 如果patch的列表比userAll的列表长,追加新的元素
|
||||||
|
userAll[key].append(patch_item)
|
||||||
|
else:
|
||||||
|
# 否则直接更新或添加key
|
||||||
|
userAll[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def implFullPlayAction(
|
||||||
|
userId: int,
|
||||||
|
currentLoginTimestamp: int,
|
||||||
|
currentLoginResult,
|
||||||
|
musicData,
|
||||||
|
userAllPatches,
|
||||||
|
debugMode=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
一份完整的上机实现,可以打 patch 来实现各种功能
|
||||||
|
需要在外部先登录并传入登录结果
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 取得 UserData
|
||||||
|
currentUserData = implGetUser_("Data", userId)
|
||||||
|
currentUserData2 = currentUserData["userData"]
|
||||||
|
|
||||||
|
# 构建并上传一个游玩记录
|
||||||
|
currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(
|
||||||
|
userId, musicData, currentUserData2, currentLoginResult["loginId"]
|
||||||
|
)
|
||||||
|
logger.debug(f"上传 UserPlayLog 结果: {currentUploadUserPlaylogApiResult}")
|
||||||
|
|
||||||
|
# 构建并上传 UserAll
|
||||||
|
retries = 0
|
||||||
|
while retries < 3:
|
||||||
|
# 计算一个特殊数
|
||||||
|
currentPlaySpecial = calcPlaySpecial()
|
||||||
|
# 生成出 UserAll
|
||||||
|
currentUserAll = generateFullUserAll(
|
||||||
|
userId,
|
||||||
|
currentLoginResult,
|
||||||
|
currentLoginTimestamp,
|
||||||
|
currentUserData2,
|
||||||
|
currentPlaySpecial,
|
||||||
|
)
|
||||||
|
# 应用参数里的补丁
|
||||||
|
applyUserAllPatches(currentUserAll, userAllPatches)
|
||||||
|
|
||||||
|
# 调试模式下直接输出数据
|
||||||
|
if debugMode:
|
||||||
|
logger.debug(
|
||||||
|
"调试模式:构建出的 UserAll 数据:"
|
||||||
|
+ json.dumps(currentUserAll, indent=4)
|
||||||
|
)
|
||||||
|
logger.info("Bye!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 建构 Json 数据
|
||||||
|
data = json.dumps(currentUserAll)
|
||||||
|
# 开始上传 UserAll
|
||||||
|
try:
|
||||||
|
currentUserAllResult = json.loads(apiSDGB(data, "UpsertUserAllApi", userId))
|
||||||
|
except SDGBRequestError:
|
||||||
|
logger.warning("上传 UserAll 出现 500. 重建数据.")
|
||||||
|
retries += 1
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
raise SDGBApiError("邪门错误")
|
||||||
|
# 成功上传后退出循环
|
||||||
|
break
|
||||||
|
else: # 重试次数超过3次
|
||||||
|
raise SDGBRequestError
|
||||||
|
|
||||||
|
logger.info("上机:结果:" + str(currentUserAllResult))
|
||||||
|
return currentUserAllResult
|
||||||
63
backend/HelperGetUserMusicDetail.py
Normal file
63
backend/HelperGetUserMusicDetail.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 获取用户成绩的各种实现
|
||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperMusicDB import getMusicTitle
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from MyConfig import testUid
|
||||||
|
|
||||||
|
# 日志设置
|
||||||
|
# log_level = "DEBUG"
|
||||||
|
# log_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS zz}</green> | <level>{level: <8}</level> | <yellow>Line {line: >4} ({file}):</yellow> <b>{message}</b>"
|
||||||
|
# logger.add(sys.stderr, level=log_level, format=log_format, colorize=True, backtrace=True, diagnose=True)
|
||||||
|
# logger.add("file.log", level=log_level, format=log_format, colorize=False, backtrace=True, diagnose=True)
|
||||||
|
|
||||||
|
|
||||||
|
def getUserMusicDetail(userId: int, nextIndex: int = 0, maxCount: int = 50) -> dict:
|
||||||
|
"""获取用户的成绩的API"""
|
||||||
|
data = json.dumps(
|
||||||
|
{"userId": int(userId), "nextIndex": nextIndex, "maxCount": maxCount}
|
||||||
|
)
|
||||||
|
return json.loads(apiSDGB(data, "GetUserMusicApi", userId))
|
||||||
|
|
||||||
|
|
||||||
|
def getUserFullMusicDetail(userId: int):
|
||||||
|
"""获取用户的全部成绩"""
|
||||||
|
currentUserMusicDetailList = []
|
||||||
|
nextIndex: int | None = None # 初始化 nextIndex
|
||||||
|
while nextIndex != 0 or nextIndex is None: # 只要还有nextIndex就一直获取获取
|
||||||
|
userMusicResponse = getUserMusicDetail(userId, nextIndex or 0)
|
||||||
|
nextIndex = userMusicResponse["nextIndex"]
|
||||||
|
logger.info(f"NextIndex: {nextIndex}")
|
||||||
|
# 处理已经没有 userMusicList 的情况
|
||||||
|
if not userMusicResponse["userMusicList"]:
|
||||||
|
break
|
||||||
|
# 只要还有 userMusicList 就一直加进去,直到全部获取完毕
|
||||||
|
for currentMusic in userMusicResponse["userMusicList"]:
|
||||||
|
for currentMusicDetail in currentMusic["userMusicDetailList"]:
|
||||||
|
if not currentMusicDetail["playCount"] > 0:
|
||||||
|
continue
|
||||||
|
currentUserMusicDetailList.append(currentMusicDetail)
|
||||||
|
return currentUserMusicDetailList
|
||||||
|
|
||||||
|
|
||||||
|
def parseUserFullMusicDetail(userFullMusicDetailList: list):
|
||||||
|
"""解析用户的全部成绩,给出一个迫真人类可读 list 套 dict"""
|
||||||
|
musicDetailList = []
|
||||||
|
for currentMusicDetail in userFullMusicDetailList:
|
||||||
|
musicDetailList.append(
|
||||||
|
{
|
||||||
|
"歌名": getMusicTitle(currentMusicDetail["musicId"]),
|
||||||
|
"难度": currentMusicDetail["level"],
|
||||||
|
"分数": currentMusicDetail["achievement"] / 10000,
|
||||||
|
"DX分数": currentMusicDetail["deluxscoreMax"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return musicDetailList
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = testUid
|
||||||
|
userFullMusicDetailList = getUserFullMusicDetail(userId)
|
||||||
|
parsedUserFullMusicDetail = parseUserFullMusicDetail(userFullMusicDetailList)
|
||||||
|
logger.info(parsedUserFullMusicDetail)
|
||||||
23
backend/HelperGetUserThing.py
Normal file
23
backend/HelperGetUserThing.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 获取用户数据的 API 实现
|
||||||
|
import rapidjson as json
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
|
||||||
|
|
||||||
|
def implGetUser_(thing: str, userId: int, noLog=False) -> dict:
|
||||||
|
"""获取用户某些数据的 API 实现,返回 Dict"""
|
||||||
|
# 获取 Json String
|
||||||
|
result = apiGetUserThing(userId, thing, noLog)
|
||||||
|
# 转换为 Dict
|
||||||
|
userthingDict = json.loads(result)
|
||||||
|
# 返回 Dict
|
||||||
|
return userthingDict
|
||||||
|
|
||||||
|
|
||||||
|
def apiGetUserThing(userId: int, thing: str, noLog=False) -> str:
|
||||||
|
"""获取用户数据的 API 请求器,返回 Json String"""
|
||||||
|
# 构建 Payload
|
||||||
|
data = json.dumps({"userId": userId})
|
||||||
|
# 发送请求
|
||||||
|
userthing_result = apiSDGB(data, "GetUser" + thing + "Api", userId, noLog)
|
||||||
|
# 返回响应
|
||||||
|
return userthing_result
|
||||||
83
backend/HelperLogInOut.py
Normal file
83
backend/HelperLogInOut.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 登录·登出实现
|
||||||
|
# 一般作为模块使用,但也可以作为 CLI 程序运行以强制登出账号。
|
||||||
|
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from Config import (
|
||||||
|
clientId,
|
||||||
|
placeId,
|
||||||
|
regionId,
|
||||||
|
)
|
||||||
|
from MyConfig import testUid
|
||||||
|
|
||||||
|
|
||||||
|
def apiLogin(timestamp: int, userId: int, noLog: bool = False) -> dict:
|
||||||
|
"""登录,返回 dict"""
|
||||||
|
data = json.dumps(
|
||||||
|
{
|
||||||
|
"userId": userId,
|
||||||
|
"accessCode": "",
|
||||||
|
"regionId": regionId,
|
||||||
|
"placeId": placeId,
|
||||||
|
"clientId": clientId,
|
||||||
|
"dateTime": timestamp,
|
||||||
|
"isContinue": False,
|
||||||
|
"genericFlag": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
login_result = json.loads(apiSDGB(data, "UserLoginApi", userId, noLog))
|
||||||
|
if not noLog:
|
||||||
|
logger.info("登录:结果:" + str(login_result))
|
||||||
|
return login_result
|
||||||
|
|
||||||
|
|
||||||
|
def apiLogout(timestamp: int, userId: int, noLog: bool = False) -> dict:
|
||||||
|
"""登出,返回 dict"""
|
||||||
|
data = json.dumps(
|
||||||
|
{
|
||||||
|
"userId": userId,
|
||||||
|
"accessCode": "",
|
||||||
|
"regionId": regionId,
|
||||||
|
"placeId": placeId,
|
||||||
|
"clientId": clientId,
|
||||||
|
"dateTime": timestamp,
|
||||||
|
"type": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logout_result = json.loads(apiSDGB(data, "UserLogoutApi", userId, noLog))
|
||||||
|
if not noLog:
|
||||||
|
logger.info("登出:结果:" + str(logout_result))
|
||||||
|
return logout_result
|
||||||
|
|
||||||
|
|
||||||
|
def generateTimestampLegacy() -> int:
|
||||||
|
"""生成一个凑合用的时间戳"""
|
||||||
|
timestamp = int(time.time()) - 60
|
||||||
|
logger.info(f"生成时间戳: {timestamp}")
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def generateTimestamp() -> int:
|
||||||
|
"""生成一个今天早上 10:00 随机偏移的时间戳"""
|
||||||
|
timestamp = int(
|
||||||
|
time.mktime(
|
||||||
|
time.strptime(time.strftime("%Y-%m-%d 10:00:00"), "%Y-%m-%d %H:%M:%S")
|
||||||
|
)
|
||||||
|
) + random.randint(-600, 600)
|
||||||
|
logger.info(f"生成时间戳: {timestamp}")
|
||||||
|
logger.info(
|
||||||
|
f"此时间戳对应的时间为: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}"
|
||||||
|
)
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("强制登出 CLI")
|
||||||
|
uid = testUid
|
||||||
|
timestamp = generateTimestamp()
|
||||||
|
apiLogout(int(timestamp), int(uid))
|
||||||
302
backend/HelperMisc.py
Normal file
302
backend/HelperMisc.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# 杂项助手函数
|
||||||
|
# 主要用于当作模块使用的时候的一些生活质量提升
|
||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
from HelperGetUserThing import implGetUser_
|
||||||
|
import unicodedata
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
|
||||||
|
from MyConfig import testUid
|
||||||
|
|
||||||
|
|
||||||
|
def numberToLetter(number):
|
||||||
|
"""
|
||||||
|
将数字转换为字母(1-26 to A-Z)
|
||||||
|
"""
|
||||||
|
if 1 <= number <= 26:
|
||||||
|
return chr(number + 64)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def maimaiVersionToHumanReadable(romVersion: str, dataVersion: str) -> str:
|
||||||
|
try:
|
||||||
|
romVersionList = romVersion.split(".")
|
||||||
|
dataVersionList = dataVersion.split(".")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法解析版本号: {romVersion} {dataVersion},错误:{e}")
|
||||||
|
return "无效版本号:无法解析"
|
||||||
|
try:
|
||||||
|
romVersionList = [int(i) for i in romVersionList]
|
||||||
|
dataVersionList = [int(i) for i in dataVersionList]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法解析版本号: {romVersion} {dataVersion},错误:{e}")
|
||||||
|
return "无效版本号:无法解读数字"
|
||||||
|
finalVersionList = []
|
||||||
|
finalVersionList.append(romVersionList[0])
|
||||||
|
# quirk
|
||||||
|
minorVer = max(romVersionList[1], dataVersionList[1])
|
||||||
|
if minorVer == 0:
|
||||||
|
finalVersionList.append("00")
|
||||||
|
else:
|
||||||
|
finalVersionList.append(minorVer)
|
||||||
|
finalVersionLetter = numberToLetter(max(romVersionList[2], dataVersionList[2]))
|
||||||
|
if finalVersionLetter:
|
||||||
|
finalVersionLetter = f"-{finalVersionLetter}"
|
||||||
|
else:
|
||||||
|
finalVersionLetter = ""
|
||||||
|
finalVersionList.append(finalVersionLetter)
|
||||||
|
|
||||||
|
if int(finalVersionList[1]) < 30:
|
||||||
|
versionStringPrefix = "CH"
|
||||||
|
else:
|
||||||
|
versionStringPrefix = "CN"
|
||||||
|
|
||||||
|
finalVersionString = f"{versionStringPrefix}{finalVersionList[0]}.{finalVersionList[1]}{finalVersionList[2]}"
|
||||||
|
return finalVersionString
|
||||||
|
|
||||||
|
|
||||||
|
levelIdDict = {"绿": 0, "黄": 1, "红": 2, "紫": 3, "白": 4, "宴": 5}
|
||||||
|
|
||||||
|
|
||||||
|
def getHalfWidthString(s):
|
||||||
|
"""全角转半角,舞萌ID用"""
|
||||||
|
return unicodedata.normalize("NFKC", s)
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableLoginErrorCode(loginResult) -> str:
|
||||||
|
"""解析登录结果并且给出中文的报错解释"""
|
||||||
|
match loginResult["returnCode"]:
|
||||||
|
case 1:
|
||||||
|
return False
|
||||||
|
case 100:
|
||||||
|
return "❌ 用户正在上机游玩,请下机后再试,或等待 15 分钟。"
|
||||||
|
case 102:
|
||||||
|
return "⚠️ 请在微信公众号内点击一次获取新的二维码,然后再试。"
|
||||||
|
case 103:
|
||||||
|
return "❌ 试图登录的账号 UID 无效,请检查账号是否正确。"
|
||||||
|
case _:
|
||||||
|
return "❌ 登录失败!这不应该发生,请反馈此问题。错误详情:" + loginResult
|
||||||
|
|
||||||
|
|
||||||
|
def checkTechnologyUseCount(userId: int) -> int:
|
||||||
|
"""猜测账号是否用了科技,0没用过,其他为用过"""
|
||||||
|
userData1 = implGetUser_("Data", userId)
|
||||||
|
userData = userData1.get("userData", {})
|
||||||
|
userRegion = implGetUser_("Region", userId)
|
||||||
|
userRegionList = userRegion.get("userRegionList", [])
|
||||||
|
|
||||||
|
playCount = userData.get("playCount", 0)
|
||||||
|
allRegionPlayCount = 0
|
||||||
|
for region in userRegionList:
|
||||||
|
allRegionPlayCount += region.get("playCount", 0)
|
||||||
|
logger.info(
|
||||||
|
f"用户 {userId} 的总游玩次数: {playCount}, 各地区游玩次数: {allRegionPlayCount}"
|
||||||
|
)
|
||||||
|
# 计算全部的 Region 加起来的游玩次数是否和 playCount 对不上,对不上就是用了科技
|
||||||
|
# 返回差值
|
||||||
|
return playCount - allRegionPlayCount
|
||||||
|
|
||||||
|
|
||||||
|
def getFriendlyUserData(userId: int) -> str:
|
||||||
|
"""生成一个(相对)友好的UserData的人话"""
|
||||||
|
userData1 = implGetUser_("Data", userId)
|
||||||
|
userData = userData1.get("userData", {})
|
||||||
|
userRegion = implGetUser_("Region", userId)
|
||||||
|
banState = userData1.get("banState")
|
||||||
|
|
||||||
|
result = f"用户: {getHalfWidthString(userData.get('userName', '未知'))}\n"
|
||||||
|
result += f"DX RATING: {userData.get('playerRating', '未知')} "
|
||||||
|
result += f"B35: {userData.get('playerOldRating', '未知')} "
|
||||||
|
result += f"B15: {userData.get('playerNewRating', '未知')}\n"
|
||||||
|
result += f"总游戏次数: {userData.get('playCount', '未知')} "
|
||||||
|
result += f"当前版本游戏次数: {userData.get('currentPlayCount', '未知')}\n"
|
||||||
|
result += f"最近登录时间: {userData.get('lastLoginDate')} "
|
||||||
|
result += f"最近登录版本: {maimaiVersionToHumanReadable(userData.get('lastRomVersion'), userData.get('lastDataVersion'))} "
|
||||||
|
result += f"最近登录地区: {userData.get('lastRegionName', '未知')}\n"
|
||||||
|
result += f"注册日期: {userData.get('firstPlayDate')} "
|
||||||
|
result += f"注册版本: {maimaiVersionToHumanReadable(userData.get('firstRomVersion'), userData.get('firstDataVersion'))}\n"
|
||||||
|
result += f"封号状态(banState): {banState}\n"
|
||||||
|
try:
|
||||||
|
logger.info(userRegion)
|
||||||
|
result += getHumanReadableRegionData(userRegion)
|
||||||
|
except Exception as e:
|
||||||
|
result += f"地区数据获取失败:{e}\n"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableRegionData(userRegion: str) -> str:
|
||||||
|
"""生成一个人类可读的地区数据"""
|
||||||
|
userRegionList = userRegion.get("userRegionList")
|
||||||
|
logger.info(userRegionList)
|
||||||
|
result = ""
|
||||||
|
for region in userRegionList:
|
||||||
|
regionName = WAHLAP_REGIONS.get(region["regionId"], "未知")
|
||||||
|
playCount = region["playCount"]
|
||||||
|
created = region["created"]
|
||||||
|
result += f"\n{regionName} 游玩次数: {playCount} 首次游玩: {created}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadablePreview(preview_json_content: str) -> str:
|
||||||
|
"""简单,粗略地解释 Preview 的 Json String 为人话。"""
|
||||||
|
previewData = json.loads(preview_json_content)
|
||||||
|
userName = getHalfWidthString(previewData["userName"])
|
||||||
|
playerRating = previewData["playerRating"]
|
||||||
|
finalString = f"用户名:{userName}\nDX RATING:{playerRating}\n"
|
||||||
|
return finalString
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableLoginBonusList(jsonString: str):
|
||||||
|
"""生成一个人类可读的 Login Bonus 的列表"""
|
||||||
|
data = json.loads(jsonString)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for bonus in data["userLoginBonusList"]:
|
||||||
|
if not bonus["isComplete"]: # 过滤已经集满的
|
||||||
|
line = f"BonusID {bonus['bonusId']} 已集 {bonus['point']} 点"
|
||||||
|
if bonus["isCurrent"]: # 如果是当前选中,追加标记
|
||||||
|
line += "(当前选中)"
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
resultString = ""
|
||||||
|
for line in result: # 转成字符串
|
||||||
|
resultString += line + "\n"
|
||||||
|
|
||||||
|
return resultString
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableTicketList(jsonString: str):
|
||||||
|
"""生成一个人类可读的 UserCharge 的列表"""
|
||||||
|
data = json.loads(jsonString)
|
||||||
|
|
||||||
|
userId = data["userId"]
|
||||||
|
length = data["length"]
|
||||||
|
userChargeList = data["userChargeList"]
|
||||||
|
|
||||||
|
result = f"UID: {userId} 票槽大小: {length} 所有记录:"
|
||||||
|
for currentItem in userChargeList:
|
||||||
|
chargeId = currentItem["chargeId"]
|
||||||
|
stock = currentItem["stock"]
|
||||||
|
purchaseDate = currentItem["purchaseDate"]
|
||||||
|
validDate = currentItem["validDate"]
|
||||||
|
|
||||||
|
result += f"\nID: {chargeId} 持有: {stock}, 购买日期: {purchaseDate}, 有效期限: {validDate}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def getHumanReadableUserData(userData) -> str:
|
||||||
|
"""生成一个人类可读的 UserData 的数据(比较详细)"""
|
||||||
|
userId = userData.get("userId")
|
||||||
|
userData = userData.get("userData", {})
|
||||||
|
banState = userData.get("banState")
|
||||||
|
logger.debug(userData)
|
||||||
|
|
||||||
|
result = f"用户名: {userData.get('userName', '未知')} "
|
||||||
|
result += f"UID: {userId}\n"
|
||||||
|
result += f"当前 RATING: {userData.get('playerRating', '未知')} "
|
||||||
|
result += f"B35: {userData.get('playerOldRating', '未知')} "
|
||||||
|
result += f"B15: {userData.get('playerNewRating', '未知')} "
|
||||||
|
result += f"最高 RATING: {userData.get('highestRating', '未知')}\n"
|
||||||
|
result += f"级别段位: {userData.get('gradeRank', '未知')} "
|
||||||
|
result += f"段位认定: {userData.get('courseRank', '未知')} "
|
||||||
|
result += f"友人对战段位: {userData.get('classRank', '未知')}\n"
|
||||||
|
result += f"总游戏次数: {userData.get('playCount', '未知')} "
|
||||||
|
result += f"当前版本游戏次数: {userData.get('currentPlayCount', '未知')}\n"
|
||||||
|
result += f"总DX分: {userData.get('totalDeluxscore', '未知')} "
|
||||||
|
result += f"绿谱总DX分: {userData.get('totalBasicDeluxscore', '未知')} "
|
||||||
|
result += f"黄谱总DX分: {userData.get('totalAdvancedDeluxscore', '未知')} "
|
||||||
|
result += f"红谱总DX分: {userData.get('totalExpertDeluxscore', '未知')} "
|
||||||
|
result += f"紫谱总DX分: {userData.get('totalMasterDeluxscore', '未知')} "
|
||||||
|
result += f"白谱总DX分: {userData.get('totalReMasterDeluxscore', '未知')}\n"
|
||||||
|
result += f"总SYNC: {userData.get('totalSync', '未知')} "
|
||||||
|
result += f"绿谱总SYNC: {userData.get('totalBasicSync', '未知')} "
|
||||||
|
result += f"黄谱总SYNC: {userData.get('totalAdvancedSync', '未知')} "
|
||||||
|
result += f"红谱总SYNC: {userData.get('totalExpertSync', '未知')} "
|
||||||
|
result += f"紫谱总SYNC: {userData.get('totalMasterSync', '未知')} "
|
||||||
|
result += f"白谱总SYNC: {userData.get('totalReMasterSync', '未知')}\n"
|
||||||
|
result += f"总分: {userData.get('totalAchievement', '未知')} "
|
||||||
|
result += f"绿谱总分: {userData.get('totalBasicAchievement', '未知')} "
|
||||||
|
result += f"黄谱总分: {userData.get('totalAdvancedAchievement', '未知')} "
|
||||||
|
result += f"红谱总分: {userData.get('totalExpertAchievement', '未知')} "
|
||||||
|
result += f"紫谱总分: {userData.get('totalMasterAchievement', '未知')} "
|
||||||
|
result += f"白谱总分: {userData.get('totalReMasterAchievement', '未知')}\n"
|
||||||
|
result += f"活动事件日期: {userData.get('eventWatchedDate')}\n"
|
||||||
|
result += f"最后 ROM 版本: {userData.get('lastRomVersion', '未知')}\n"
|
||||||
|
result += f"最后数据版本: {userData.get('lastDataVersion', '未知')}\n"
|
||||||
|
result += f"最后登录时间: {userData.get('lastLoginDate')}\n"
|
||||||
|
result += f"最后游戏时间: {userData.get('lastPlayDate')}\n"
|
||||||
|
result += f"最后双人登录时间: {userData.get('lastPairLoginDate')}\n"
|
||||||
|
result += f"最后免费游戏时间: {userData.get('lastTrialPlayDate')}\n"
|
||||||
|
result += f"最后游戏花费: {userData.get('lastPlayCredit', '未知')}\n"
|
||||||
|
result += f"最后地区 ID: {userData.get('lastRegionId', '未知')}\n"
|
||||||
|
result += f"最后地区名称: {userData.get('lastRegionName', '未知')}\n"
|
||||||
|
result += f"最后选择功能票: {userData.get('lastSelectTicket', '未知')}\n"
|
||||||
|
result += f"最后一次段位认定: {userData.get('lastSelectCourse', '未知')}\n"
|
||||||
|
result += f"最后一次 Course 计数: {userData.get('lastCountCourse', '未知')}\n"
|
||||||
|
result += f"注册 ROM 版本: {userData.get('firstRomVersion', '未知')}\n"
|
||||||
|
result += f"注册数据版本: {userData.get('firstDataVersion', '未知')}\n"
|
||||||
|
result += f"注册日期: {userData.get('firstPlayDate')}\n"
|
||||||
|
result += f"总觉醒: {userData.get('totalAwake', '未知')}\n"
|
||||||
|
result += f"签到日期: {userData.get('dailyCourseBonusDate')}\n"
|
||||||
|
result += f"跑图存储距离: {userData.get('mapStock', '未知')}m\n"
|
||||||
|
result += f"封号状态: {banState}\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
WAHLAP_REGIONS = {
|
||||||
|
1: "北京",
|
||||||
|
2: "重庆",
|
||||||
|
3: "上海",
|
||||||
|
4: "天津",
|
||||||
|
5: "安徽",
|
||||||
|
6: "福建",
|
||||||
|
7: "甘肃",
|
||||||
|
8: "广东",
|
||||||
|
9: "贵州",
|
||||||
|
10: "海南",
|
||||||
|
11: "河北",
|
||||||
|
12: "黑龙江",
|
||||||
|
13: "河南",
|
||||||
|
14: "湖北",
|
||||||
|
15: "湖南",
|
||||||
|
16: "江苏",
|
||||||
|
17: "江西",
|
||||||
|
18: "吉林",
|
||||||
|
19: "辽宁",
|
||||||
|
20: "青海",
|
||||||
|
21: "陕西",
|
||||||
|
22: "山东",
|
||||||
|
23: "山西",
|
||||||
|
24: "四川",
|
||||||
|
25: "(未知25)",
|
||||||
|
26: "云南",
|
||||||
|
27: "浙江",
|
||||||
|
28: "广西",
|
||||||
|
29: "内蒙古",
|
||||||
|
30: "宁夏",
|
||||||
|
31: "新疆",
|
||||||
|
32: "西藏",
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# test version string convert
|
||||||
|
print(maimaiVersionToHumanReadable("1.20.0", "1.0.0"))
|
||||||
|
print(maimaiVersionToHumanReadable("1.41.00", "1.40.11"))
|
||||||
|
print(maimaiVersionToHumanReadable("1.00.00", "1.00.00"))
|
||||||
|
|
||||||
|
userId = testUid
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(checkTechnologyUseCount(userId))
|
||||||
|
# logger.info(apiQueryTicket(userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
14
backend/HelperMusicDB.py
Normal file
14
backend/HelperMusicDB.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from MusicDB import musicDB
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def getMusicTitle(musicId: int) -> str:
|
||||||
|
"""从数据库获取音乐的标题"""
|
||||||
|
# logger.debug(f"查询歌名: {musicId}")
|
||||||
|
musicInfo = musicDB.get(musicId)
|
||||||
|
if not musicInfo:
|
||||||
|
logger.warning(f"数据库里未找到此歌曲: {musicId}")
|
||||||
|
return "R_ERR_MUSIC_ID_NOT_IN_DATABASE"
|
||||||
|
musicName = musicInfo.get("name")
|
||||||
|
# logger.debug(f"成功查询到歌名: {musicName}")
|
||||||
|
return musicName
|
||||||
97
backend/HelperUnlockThing.py
Normal file
97
backend/HelperUnlockThing.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 解锁东西的一个通用的助手,不可独立使用
|
||||||
|
from HelperFullPlay import implFullPlayAction
|
||||||
|
|
||||||
|
|
||||||
|
def implUnlockThing(
|
||||||
|
newUserItemList, userId: int, currentLoginTimestamp: int, currentLoginResult
|
||||||
|
) -> str:
|
||||||
|
musicData = {
|
||||||
|
"musicId": 11538, # Amber Chronicle
|
||||||
|
"level": 0,
|
||||||
|
"playCount": 1,
|
||||||
|
"achievement": 0,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"deluxscoreMax": 0,
|
||||||
|
"scoreRank": 0,
|
||||||
|
"extNum1": 0,
|
||||||
|
}
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1",
|
||||||
|
"userItemList": newUserItemList,
|
||||||
|
"isNewItemList": "1" * len(newUserItemList),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
itemKindDict = {
|
||||||
|
"PLATE": 1, # 姓名框
|
||||||
|
"TITLE": 2, # 称号
|
||||||
|
"ICON": 3, # 头像
|
||||||
|
"MUSIC": 5, # 歌
|
||||||
|
"MUSIC_MASTER": 6, # 紫谱
|
||||||
|
"MUSIC_RE_MASTER": 7, # 白谱
|
||||||
|
"CHARACTER": 9, # 旅行伙伴
|
||||||
|
"PARTNER": 10, # 搭档
|
||||||
|
"FRAME": 11, # 背景板
|
||||||
|
"TICKET": 12, # 功能票
|
||||||
|
# "PRESENT": 4, # ?
|
||||||
|
# "MUSIC_STRONG": 8, # ?
|
||||||
|
}
|
||||||
|
|
||||||
|
itemKindzhCNDict = {
|
||||||
|
"姓名框": "PLATE",
|
||||||
|
"称号": "TITLE",
|
||||||
|
"头像": "ICON",
|
||||||
|
"歌": "MUSIC",
|
||||||
|
"紫谱": "MUSIC_MASTER",
|
||||||
|
"白谱": "MUSIC_RE_MASTER",
|
||||||
|
"旅行伙伴": "CHARACTER",
|
||||||
|
"搭档": "PARTNER",
|
||||||
|
"背景板": "FRAME",
|
||||||
|
"功能票": "TICKET",
|
||||||
|
# "礼物": "PRESENT",
|
||||||
|
# "STRONG": "MUSIC_STRONG",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Splash = 2020
|
||||||
|
# UNiVERSE = 2022
|
||||||
|
# FESTiVAL = 2023
|
||||||
|
# BUDDiES = 2024
|
||||||
|
# PRiSM = 2025
|
||||||
|
|
||||||
|
partnerList = {
|
||||||
|
"1": "迪拉熊",
|
||||||
|
"17": "青柠熊&柠檬熊",
|
||||||
|
"29": "青柠熊&柠檬熊(2024)",
|
||||||
|
"11": "乙姫",
|
||||||
|
"18": "乙姫(Splash)",
|
||||||
|
"28": "乙姫(2024)",
|
||||||
|
"12": "拉兹",
|
||||||
|
"23": "拉兹(2023)",
|
||||||
|
"30": "拉兹 (BUDDiES)",
|
||||||
|
"13": "雪纺",
|
||||||
|
"24": "雪纺(2023)",
|
||||||
|
"14": "莎露朵",
|
||||||
|
"25": "莎露朵(2023)",
|
||||||
|
"31": "莎露朵 (PRiSM)",
|
||||||
|
"15": "夏玛",
|
||||||
|
"19": "夏玛(UNiVERSE)",
|
||||||
|
"16": "咪璐库",
|
||||||
|
"21": "小咪璐库",
|
||||||
|
"32": "咪璐库 (PRiSM)",
|
||||||
|
"20": "咪璐库(UNiVERSE)",
|
||||||
|
"22": "百合咲美香",
|
||||||
|
"26": "黒姫",
|
||||||
|
"27": "俊达萌",
|
||||||
|
"33": "超天酱",
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, partner in partnerList.items():
|
||||||
|
print()
|
||||||
153
backend/HelperUploadUserPlayLog.py
Normal file
153
backend/HelperUploadUserPlayLog.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# 上传一个占位用的游玩记录的 API 实现
|
||||||
|
|
||||||
|
import rapidjson as json
|
||||||
|
import pytz
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from API_TitleServer import apiSDGB
|
||||||
|
from Config import (
|
||||||
|
placeId,
|
||||||
|
placeName,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apiUploadUserPlaylog(
|
||||||
|
userId: int, musicDataToBeUploaded, currentUserData2, loginId: int
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
上传一个 UserPlayLog。
|
||||||
|
注意:成绩为随机的空成绩,只用作占位
|
||||||
|
返回 Json String。"""
|
||||||
|
|
||||||
|
# 构建一个 PlayLog
|
||||||
|
data = json.dumps(
|
||||||
|
{
|
||||||
|
"userId": int(userId),
|
||||||
|
"userPlaylogList": [
|
||||||
|
{
|
||||||
|
"userId": 0,
|
||||||
|
"orderId": 0,
|
||||||
|
"playlogId": loginId,
|
||||||
|
"version": 1051000,
|
||||||
|
"placeId": placeId,
|
||||||
|
"placeName": placeName,
|
||||||
|
"loginDate": int(time.time()), # 似乎和登录timestamp不同
|
||||||
|
"playDate": datetime.now(pytz.timezone("Asia/Shanghai")).strftime(
|
||||||
|
"%Y-%m-%d"
|
||||||
|
),
|
||||||
|
"userPlayDate": datetime.now(
|
||||||
|
pytz.timezone("Asia/Shanghai")
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
+ ".0",
|
||||||
|
"type": 0,
|
||||||
|
"musicId": int(musicDataToBeUploaded["musicId"]),
|
||||||
|
"level": int(musicDataToBeUploaded["level"]),
|
||||||
|
"trackNo": 1,
|
||||||
|
"vsMode": 0,
|
||||||
|
"vsUserName": "",
|
||||||
|
"vsStatus": 0,
|
||||||
|
"vsUserRating": 0,
|
||||||
|
"vsUserAchievement": 0,
|
||||||
|
"vsUserGradeRank": 0,
|
||||||
|
"vsRank": 0,
|
||||||
|
"playerNum": 1,
|
||||||
|
"playedUserId1": 0,
|
||||||
|
"playedUserName1": "",
|
||||||
|
"playedMusicLevel1": 0,
|
||||||
|
"playedUserId2": 0,
|
||||||
|
"playedUserName2": "",
|
||||||
|
"playedMusicLevel2": 0,
|
||||||
|
"playedUserId3": 0,
|
||||||
|
"playedUserName3": "",
|
||||||
|
"playedMusicLevel3": 0,
|
||||||
|
"characterId1": currentUserData2["charaSlot"][0],
|
||||||
|
"characterLevel1": random.randint(1000, 6500),
|
||||||
|
"characterAwakening1": 5,
|
||||||
|
"characterId2": currentUserData2["charaSlot"][1],
|
||||||
|
"characterLevel2": random.randint(1000, 6500),
|
||||||
|
"characterAwakening2": 5,
|
||||||
|
"characterId3": currentUserData2["charaSlot"][2],
|
||||||
|
"characterLevel3": random.randint(1000, 6500),
|
||||||
|
"characterAwakening3": 5,
|
||||||
|
"characterId4": currentUserData2["charaSlot"][3],
|
||||||
|
"characterLevel4": random.randint(1000, 6500),
|
||||||
|
"characterAwakening4": 5,
|
||||||
|
"characterId5": currentUserData2["charaSlot"][4],
|
||||||
|
"characterLevel5": random.randint(1000, 6500),
|
||||||
|
"characterAwakening5": 5,
|
||||||
|
"achievement": int(musicDataToBeUploaded["achievement"]),
|
||||||
|
"deluxscore": int(musicDataToBeUploaded["deluxscoreMax"]),
|
||||||
|
"scoreRank": int(musicDataToBeUploaded["scoreRank"]),
|
||||||
|
"maxCombo": 0,
|
||||||
|
"totalCombo": random.randint(700, 900),
|
||||||
|
"maxSync": 0,
|
||||||
|
"totalSync": 0,
|
||||||
|
"tapCriticalPerfect": 0,
|
||||||
|
"tapPerfect": 0,
|
||||||
|
"tapGreat": 0,
|
||||||
|
"tapGood": 0,
|
||||||
|
"tapMiss": random.randint(1, 10),
|
||||||
|
"holdCriticalPerfect": 0,
|
||||||
|
"holdPerfect": 0,
|
||||||
|
"holdGreat": 0,
|
||||||
|
"holdGood": 0,
|
||||||
|
"holdMiss": random.randint(1, 15),
|
||||||
|
"slideCriticalPerfect": 0,
|
||||||
|
"slidePerfect": 0,
|
||||||
|
"slideGreat": 0,
|
||||||
|
"slideGood": 0,
|
||||||
|
"slideMiss": random.randint(1, 15),
|
||||||
|
"touchCriticalPerfect": 0,
|
||||||
|
"touchPerfect": 0,
|
||||||
|
"touchGreat": 0,
|
||||||
|
"touchGood": 0,
|
||||||
|
"touchMiss": random.randint(1, 15),
|
||||||
|
"breakCriticalPerfect": 0,
|
||||||
|
"breakPerfect": 0,
|
||||||
|
"breakGreat": 0,
|
||||||
|
"breakGood": 0,
|
||||||
|
"breakMiss": random.randint(1, 15),
|
||||||
|
"isTap": True,
|
||||||
|
"isHold": True,
|
||||||
|
"isSlide": True,
|
||||||
|
"isTouch": True,
|
||||||
|
"isBreak": True,
|
||||||
|
"isCriticalDisp": True,
|
||||||
|
"isFastLateDisp": True,
|
||||||
|
"fastCount": 0,
|
||||||
|
"lateCount": 0,
|
||||||
|
"isAchieveNewRecord": True,
|
||||||
|
"isDeluxscoreNewRecord": True,
|
||||||
|
"comboStatus": 0,
|
||||||
|
"syncStatus": 0,
|
||||||
|
"isClear": False,
|
||||||
|
"beforeRating": currentUserData2["playerRating"],
|
||||||
|
"afterRating": currentUserData2["playerRating"],
|
||||||
|
"beforeGrade": 0,
|
||||||
|
"afterGrade": 0,
|
||||||
|
"afterGradeRank": 1,
|
||||||
|
"beforeDeluxRating": currentUserData2["playerRating"],
|
||||||
|
"afterDeluxRating": currentUserData2["playerRating"],
|
||||||
|
"isPlayTutorial": False,
|
||||||
|
"isEventMode": False,
|
||||||
|
"isFreedomMode": False,
|
||||||
|
"playMode": 0,
|
||||||
|
"isNewFree": False,
|
||||||
|
"trialPlayAchievement": -1,
|
||||||
|
"extNum1": 0,
|
||||||
|
"extNum2": 0,
|
||||||
|
"extNum4": 3020,
|
||||||
|
"extBool1": False,
|
||||||
|
"extBool2": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 发送请求
|
||||||
|
result = apiSDGB(data, "UploadUserPlaylogListApi", userId)
|
||||||
|
logger.info("上传游玩记录:结果:" + str(result))
|
||||||
|
# 返回响应
|
||||||
|
return result
|
||||||
270
backend/HelperUserAll.py
Normal file
270
backend/HelperUserAll.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# UserAll 有关的一些辅助函数
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from datetime import datetime
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperGetUserThing import implGetUser_
|
||||||
|
from HelperGetUserMusicDetail import getUserMusicDetail
|
||||||
|
|
||||||
|
from Config import (
|
||||||
|
clientId,
|
||||||
|
placeName,
|
||||||
|
placeId,
|
||||||
|
regionId,
|
||||||
|
regionName,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isNewMusicType(userId, musicId, level) -> str:
|
||||||
|
"""判断这首 musicId 在 isNewMusicDetailList 应该填什么
|
||||||
|
0: Edit
|
||||||
|
1: Insert
|
||||||
|
一次只能处理一首歌,所以返回值是 str
|
||||||
|
|
||||||
|
未完工,仅供测试
|
||||||
|
"""
|
||||||
|
userMusicDetailList = getUserMusicDetail(userId, musicId, 1)["userMusicList"][0][
|
||||||
|
"userMusicDetailList"
|
||||||
|
]
|
||||||
|
logger.info(userMusicDetailList)
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
userMusicDetailList[0]["musicId"] == musicId
|
||||||
|
and userMusicDetailList[0]["level"] == level
|
||||||
|
):
|
||||||
|
logger.info(f"We think {musicId} Level {level} should use EDIT.")
|
||||||
|
return "0"
|
||||||
|
except Exception:
|
||||||
|
return "1"
|
||||||
|
|
||||||
|
|
||||||
|
def generateFullUserAll(
|
||||||
|
userId,
|
||||||
|
currentLoginResult,
|
||||||
|
currentLoginTimestamp,
|
||||||
|
currentUserData2,
|
||||||
|
currentPlaySpecial,
|
||||||
|
):
|
||||||
|
"""从服务器取得必要的数据并构建一个比较完整的 UserAll"""
|
||||||
|
|
||||||
|
# 先构建一个基础 UserAll
|
||||||
|
currentUserAll = generateUserAllData(
|
||||||
|
userId,
|
||||||
|
currentLoginResult,
|
||||||
|
currentLoginTimestamp,
|
||||||
|
currentUserData2,
|
||||||
|
currentPlaySpecial,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 然后从服务器取得必要的数据
|
||||||
|
currentUserExtend = implGetUser_("Extend", userId, True)
|
||||||
|
currentUserOption = implGetUser_("Option", userId, True)
|
||||||
|
currentUserRating = implGetUser_("Rating", userId, True)
|
||||||
|
currentUserActivity = implGetUser_("Activity", userId, True)
|
||||||
|
currentUserCharge = implGetUser_("Charge", userId, True)
|
||||||
|
currentUserMissionData = implGetUser_("MissionData", userId, True)
|
||||||
|
|
||||||
|
# 把这些数据都追加进去
|
||||||
|
currentUserAll["upsertUserAll"]["userExtend"] = [currentUserExtend["userExtend"]]
|
||||||
|
currentUserAll["upsertUserAll"]["userOption"] = [currentUserOption["userOption"]]
|
||||||
|
currentUserAll["upsertUserAll"]["userRatingList"] = [
|
||||||
|
currentUserRating["userRating"]
|
||||||
|
]
|
||||||
|
currentUserAll["upsertUserAll"]["userActivityList"] = [
|
||||||
|
currentUserActivity["userActivity"]
|
||||||
|
]
|
||||||
|
currentUserAll["upsertUserAll"]["userChargeList"] = currentUserCharge[
|
||||||
|
"userChargeList"
|
||||||
|
]
|
||||||
|
currentUserAll["upsertUserAll"]["userWeeklyData"] = currentUserMissionData[
|
||||||
|
"userWeeklyData"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 完事
|
||||||
|
return currentUserAll
|
||||||
|
|
||||||
|
|
||||||
|
def generateUserAllData(
|
||||||
|
userId,
|
||||||
|
currentLoginResult,
|
||||||
|
currentLoginTimestamp,
|
||||||
|
currentUserData2,
|
||||||
|
currentPlaySpecial,
|
||||||
|
):
|
||||||
|
"""构建一个非常基础的 UserAll 数据,必须手动填充一些数据"""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"userId": userId,
|
||||||
|
"playlogId": currentLoginResult["loginId"],
|
||||||
|
"isEventMode": False,
|
||||||
|
"isFreePlay": False,
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userData": [
|
||||||
|
{
|
||||||
|
"accessCode": "",
|
||||||
|
"userName": currentUserData2["userName"],
|
||||||
|
"isNetMember": 1,
|
||||||
|
"point": currentUserData2["point"],
|
||||||
|
"totalPoint": currentUserData2["totalPoint"],
|
||||||
|
"iconId": currentUserData2["iconId"],
|
||||||
|
"plateId": currentUserData2["plateId"],
|
||||||
|
"titleId": currentUserData2["titleId"],
|
||||||
|
"partnerId": currentUserData2["partnerId"],
|
||||||
|
"frameId": currentUserData2["frameId"],
|
||||||
|
"selectMapId": currentUserData2["selectMapId"],
|
||||||
|
"totalAwake": currentUserData2["totalAwake"],
|
||||||
|
"gradeRating": currentUserData2["gradeRating"],
|
||||||
|
"musicRating": currentUserData2["musicRating"],
|
||||||
|
"playerRating": currentUserData2["playerRating"],
|
||||||
|
"highestRating": currentUserData2["highestRating"],
|
||||||
|
"gradeRank": currentUserData2["gradeRank"],
|
||||||
|
"classRank": currentUserData2["classRank"],
|
||||||
|
"courseRank": currentUserData2["courseRank"],
|
||||||
|
"charaSlot": currentUserData2["charaSlot"],
|
||||||
|
"charaLockSlot": currentUserData2["charaLockSlot"],
|
||||||
|
"contentBit": currentUserData2["contentBit"],
|
||||||
|
"playCount": currentUserData2["playCount"],
|
||||||
|
"currentPlayCount": currentUserData2["currentPlayCount"],
|
||||||
|
"renameCredit": 0,
|
||||||
|
"mapStock": currentUserData2["mapStock"],
|
||||||
|
"eventWatchedDate": currentUserData2["eventWatchedDate"],
|
||||||
|
"lastGameId": "SDGB",
|
||||||
|
"lastRomVersion": currentUserData2["lastRomVersion"],
|
||||||
|
"lastDataVersion": currentUserData2["lastDataVersion"],
|
||||||
|
# "lastLoginDate": currentLoginResult['lastLoginDate'], # sb
|
||||||
|
"lastLoginDate": currentUserData2["lastLoginDate"], # 等待测试
|
||||||
|
"lastPlayDate": datetime.now(
|
||||||
|
pytz.timezone("Asia/Shanghai")
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
+ ".0",
|
||||||
|
"lastPlayCredit": 1,
|
||||||
|
"lastPlayMode": 0,
|
||||||
|
"lastPlaceId": placeId,
|
||||||
|
"lastPlaceName": placeName,
|
||||||
|
"lastAllNetId": 0,
|
||||||
|
"lastRegionId": regionId,
|
||||||
|
"lastRegionName": regionName,
|
||||||
|
"lastClientId": clientId,
|
||||||
|
"lastCountryCode": "CHN",
|
||||||
|
"lastSelectEMoney": 0,
|
||||||
|
"lastSelectTicket": 0,
|
||||||
|
"lastSelectCourse": currentUserData2["lastSelectCourse"],
|
||||||
|
"lastCountCourse": 0,
|
||||||
|
"firstGameId": "SDGB",
|
||||||
|
"firstRomVersion": currentUserData2["firstRomVersion"],
|
||||||
|
"firstDataVersion": currentUserData2["firstDataVersion"],
|
||||||
|
"firstPlayDate": currentUserData2["firstPlayDate"],
|
||||||
|
"compatibleCmVersion": currentUserData2["compatibleCmVersion"],
|
||||||
|
"dailyBonusDate": currentUserData2["dailyBonusDate"],
|
||||||
|
"dailyCourseBonusDate": currentUserData2["dailyCourseBonusDate"],
|
||||||
|
"lastPairLoginDate": currentUserData2["lastPairLoginDate"],
|
||||||
|
"lastTrialPlayDate": currentUserData2["lastTrialPlayDate"],
|
||||||
|
"playVsCount": 0,
|
||||||
|
"playSyncCount": 0,
|
||||||
|
"winCount": 0,
|
||||||
|
"helpCount": 0,
|
||||||
|
"comboCount": 0,
|
||||||
|
"totalDeluxscore": currentUserData2["totalDeluxscore"],
|
||||||
|
"totalBasicDeluxscore": currentUserData2["totalBasicDeluxscore"],
|
||||||
|
"totalAdvancedDeluxscore": currentUserData2[
|
||||||
|
"totalAdvancedDeluxscore"
|
||||||
|
],
|
||||||
|
"totalExpertDeluxscore": currentUserData2["totalExpertDeluxscore"],
|
||||||
|
"totalMasterDeluxscore": currentUserData2["totalMasterDeluxscore"],
|
||||||
|
"totalReMasterDeluxscore": currentUserData2[
|
||||||
|
"totalReMasterDeluxscore"
|
||||||
|
],
|
||||||
|
"totalSync": currentUserData2["totalSync"],
|
||||||
|
"totalBasicSync": currentUserData2["totalBasicSync"],
|
||||||
|
"totalAdvancedSync": currentUserData2["totalAdvancedSync"],
|
||||||
|
"totalExpertSync": currentUserData2["totalExpertSync"],
|
||||||
|
"totalMasterSync": currentUserData2["totalMasterSync"],
|
||||||
|
"totalReMasterSync": currentUserData2["totalReMasterSync"],
|
||||||
|
"totalAchievement": currentUserData2["totalAchievement"],
|
||||||
|
"totalBasicAchievement": currentUserData2["totalBasicAchievement"],
|
||||||
|
"totalAdvancedAchievement": currentUserData2[
|
||||||
|
"totalAdvancedAchievement"
|
||||||
|
],
|
||||||
|
"totalExpertAchievement": currentUserData2[
|
||||||
|
"totalExpertAchievement"
|
||||||
|
],
|
||||||
|
"totalMasterAchievement": currentUserData2[
|
||||||
|
"totalMasterAchievement"
|
||||||
|
],
|
||||||
|
"totalReMasterAchievement": currentUserData2[
|
||||||
|
"totalReMasterAchievement"
|
||||||
|
],
|
||||||
|
"playerOldRating": currentUserData2["playerOldRating"],
|
||||||
|
"playerNewRating": currentUserData2["playerNewRating"],
|
||||||
|
"banState": 0,
|
||||||
|
"friendRegistSkip": currentUserData2["friendRegistSkip"],
|
||||||
|
"dateTime": currentLoginTimestamp,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userExtend": [], # 需要填上
|
||||||
|
"userOption": [], # 需要填上
|
||||||
|
"userGhost": [],
|
||||||
|
"userCharacterList": [],
|
||||||
|
"userMapList": [],
|
||||||
|
"userLoginBonusList": [],
|
||||||
|
"userRatingList": [], # 需要填上
|
||||||
|
"userItemList": [], # 可选,但经常要填上
|
||||||
|
"userMusicDetailList": [], # 需要填上
|
||||||
|
"userCourseList": [],
|
||||||
|
"userFriendSeasonRankingList": [],
|
||||||
|
"userChargeList": [], # 需要填上
|
||||||
|
"userFavoriteList": [],
|
||||||
|
"userActivityList": [], # 需要填上
|
||||||
|
"userMissionDataList": [],
|
||||||
|
"userWeeklyData": [], # 应该需要填上
|
||||||
|
"userGamePlaylogList": [
|
||||||
|
{
|
||||||
|
"playlogId": currentLoginResult["loginId"],
|
||||||
|
"version": "1.51.00",
|
||||||
|
"playDate": datetime.now(pytz.timezone("Asia/Shanghai")).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
+ ".0",
|
||||||
|
"playMode": 0,
|
||||||
|
"useTicketId": -1,
|
||||||
|
"playCredit": 1,
|
||||||
|
"playTrack": 1,
|
||||||
|
"clientId": clientId,
|
||||||
|
"isPlayTutorial": False,
|
||||||
|
"isEventMode": False,
|
||||||
|
"isNewFree": False,
|
||||||
|
"playCount": currentUserData2["playCount"],
|
||||||
|
"playSpecial": currentPlaySpecial,
|
||||||
|
"playOtherUserId": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user2pPlaylog": {
|
||||||
|
"userId1": 0,
|
||||||
|
"userId2": 0,
|
||||||
|
"userName1": "",
|
||||||
|
"userName2": "",
|
||||||
|
"regionId": 0,
|
||||||
|
"placeId": 0,
|
||||||
|
"user2pPlaylogDetailList": [],
|
||||||
|
},
|
||||||
|
"userIntimateList": [],
|
||||||
|
"userShopItemStockList": [],
|
||||||
|
"userGetPointList": [],
|
||||||
|
"userTradeItemList": [],
|
||||||
|
"userFavoritemusicList": [],
|
||||||
|
"userKaleidxScopeList": [],
|
||||||
|
"isNewCharacterList": "",
|
||||||
|
"isNewMapList": "",
|
||||||
|
"isNewLoginBonusList": "",
|
||||||
|
"isNewItemList": "",
|
||||||
|
"isNewMusicDetailList": "", # 可选但经常要填上
|
||||||
|
"isNewCourseList": "0",
|
||||||
|
"isNewFavoriteList": "",
|
||||||
|
"isNewFriendSeasonRankingList": "",
|
||||||
|
"isNewUserIntimateList": "",
|
||||||
|
"isNewFavoritemusicList": "",
|
||||||
|
"isNewKaleidxScopeList": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return data
|
||||||
23
backend/MusicDB.py
Normal file
23
backend/MusicDB.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import rapidjson as json
|
||||||
|
from Config import *
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
|
# 定义音乐数据库的类型注解
|
||||||
|
MusicDBType = Dict[int, Dict[str, Union[int, str]]]
|
||||||
|
|
||||||
|
# 将 '__all__' 用于模块导出声明
|
||||||
|
__all__ = ["musicDB"]
|
||||||
|
|
||||||
|
# 读取并解析 JSON 文件
|
||||||
|
try:
|
||||||
|
with open(musicDBPath, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
try:
|
||||||
|
with open(musicDBPathFallback, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
raise FileNotFoundError("musicDB.json 文件不存在!")
|
||||||
|
|
||||||
|
# 将 JSON 数据转换为指定格式的字典
|
||||||
|
musicDB: MusicDBType = {int(k): v for k, v in data.items()}
|
||||||
2
backend/README.md
Normal file
2
backend/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# maimai DX API
|
||||||
|
API of maimai DX CN(SDGB).
|
||||||
33
backend/Standalone/DecryptAuthLiteTraffic.py
Normal file
33
backend/Standalone/DecryptAuthLiteTraffic.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 解密国服 Auth Lite 的包
|
||||||
|
# 仅测试过 PowerOn 请求,其他包不确定是否适用
|
||||||
|
# 仅适用国服 DX2024,其他未测试
|
||||||
|
|
||||||
|
# 完全 Standalone,不依赖于其他文件
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from urllib.parse import unquote
|
||||||
|
from Crypto.Util.Padding import unpad
|
||||||
|
|
||||||
|
# 密钥从 HDD 提取
|
||||||
|
LITE_AUTH_KEY = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
|
||||||
|
LITE_AUTH_IV = bytes.fromhex('00000000000000000000000000000000')
|
||||||
|
|
||||||
|
# 填入你抓包得到的想解密的数据的 base64 编码
|
||||||
|
base64String = "N0PyrjawH/7qA8A28y++3txHDsKuAs5+nib751QiNlJYigvwldPaG7xd0WYXvgqlWY16JIy38GQ8+M4ttaWRNfpWy9l29pC2h2abd4VGhIeWGLbOjc2Bthqhibui76vi4dW+05TsPiyXbOsqHFzScvdByKUtZUobZgrnr/WW+YqRIUdw/ZHBmKBY81JivnVH9AkEyCCP9xubYMjDqi65WhDpcrdMk5nUjHq/O7R1eXr12Es9gXDUruy/H4M7eMt+4kFSDCGpLSFwAEDhba6rpOz0n588nfvXXFlZ+a3ZsZSBYAJPBZ795Ck8ZDIYnEMWMV5nk6qPc2HiBF9ZZw88FlATGC8NqsTSjGX6JJXWDApUaSF5obXMu4LTmMMr0KDt2fQ6VQPkLnTgJ6tsJv1iAQvtcZ9ymn3I4S0XWXGmEq8r7XE7D+pnSJyjUn7bSXb6HOzCQtQc9XYmIbylS2sNkiDXywrxVgmiAXc4Ta8M9aNUb+81QrKj6qqC06DzdYQNBFRxb78X3nGYECEmKBsxIOR7Mf/ZqWYtA28Ob9H5CCUmjLvUaoQ+htJMHfQ9fvvevfu+FxtlJXfw+3UQDiQ1xvSZe2NMCgkLOuwqZ5/5PyoAV9MKzRXT4hBzDoiAIt7bzOH9JcNJkjUtLAjXbnwN6M6zUKpgMK4WYeCwUffNy21GbLVtfIxZZbVhK8A6Ni7j"
|
||||||
|
|
||||||
|
def auth_lite_decrypt(ciphertext: bytes) -> str:
|
||||||
|
# 解密并去除填充
|
||||||
|
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
|
||||||
|
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||||
|
# 提取内容并解码
|
||||||
|
content = decrypted_data[16:] # 去除头部的16字节
|
||||||
|
return content.decode('utf-8').strip()
|
||||||
|
|
||||||
|
# 解码 base64
|
||||||
|
decodedData = base64.b64decode(base64String)
|
||||||
|
# 解密数据
|
||||||
|
decryptedData = auth_lite_decrypt(decodedData)
|
||||||
|
print(decryptedData)
|
||||||
|
# 解码 URL 编码
|
||||||
|
print(unquote(decryptedData))
|
||||||
74
backend/Standalone/DecryptTitleServerTraffic.py
Normal file
74
backend/Standalone/DecryptTitleServerTraffic.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# 解密从 HDD 抓包得到的数据
|
||||||
|
# 兼容 PRiSM 和 CN 2024
|
||||||
|
|
||||||
|
# 完全 Standalone,不依赖于其他文件
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import zlib
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import unpad, pad
|
||||||
|
|
||||||
|
# CN 2024
|
||||||
|
AES_KEY_SDGB_1_40 = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"
|
||||||
|
AES_IV_SDGB_1_40 = ";;KjR1C3hgB1ovXa"
|
||||||
|
|
||||||
|
# 国际服 PRiSM
|
||||||
|
AES_KEY_SDGA_1_50 = "A;mv5YUpHBK3YxTy5KB^[;5]C2AL50Bq"
|
||||||
|
AES_IV_SDGA_1_50 = "9FM:sd9xA91X14v]"
|
||||||
|
|
||||||
|
AES_KEY_SDGB_1_50 = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"
|
||||||
|
AES_IV_SDGB_1_50 = "d6xHIKq]1J]Dt^ue"
|
||||||
|
|
||||||
|
class AESPKCS7:
|
||||||
|
# 实现了 maimai 通讯所用的 AES 加密的类
|
||||||
|
def __init__(self, key: str, iv: str):
|
||||||
|
self.key = key.encode('utf-8')
|
||||||
|
self.iv = iv.encode('utf-8')
|
||||||
|
self.mode = AES.MODE_CBC
|
||||||
|
# 加密
|
||||||
|
def encrypt(self, content: str) -> bytes:
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
content_padded = pad(content.encode(), AES.block_size)
|
||||||
|
encrypted_bytes = cipher.encrypt(content_padded)
|
||||||
|
return encrypted_bytes
|
||||||
|
# 解密
|
||||||
|
def decrypt(self, encrypted_content: bytes) -> str:
|
||||||
|
cipher = AES.new(self.key, self.mode, self.iv)
|
||||||
|
decrypted_padded = cipher.decrypt(encrypted_content)
|
||||||
|
decrypted = unpad(decrypted_padded, AES.block_size)
|
||||||
|
return decrypted
|
||||||
|
|
||||||
|
def main_sdga():
|
||||||
|
# 填入你的想解密的数据的 base64 编码
|
||||||
|
base64_encoded_data = "KSGm2qo7qVHz1wrK15PckYC5/kLjKcTtEXOgHeHt1Xn6DPdo3pltoPLADHpe8+Wq"
|
||||||
|
|
||||||
|
aes = AESPKCS7(AES_KEY_SDGA_1_50, AES_IV_SDGA_1_50)
|
||||||
|
|
||||||
|
# 首先解码 base64
|
||||||
|
decodedData = base64.b64decode(base64_encoded_data)
|
||||||
|
# 然后解密数据,PRiSM 是先压缩再加密(which is 正确做法)
|
||||||
|
decryptedData = aes.decrypt(decodedData)
|
||||||
|
# 解压数据
|
||||||
|
decompressedData = zlib.decompress(decryptedData)
|
||||||
|
|
||||||
|
print(str(decompressedData))
|
||||||
|
|
||||||
|
def main_sdgb140():
|
||||||
|
# 填入你的想解密的数据的 base64 编码
|
||||||
|
base64_encoded_data = "eJyrTVvpuGwCR32OdodwtVXZ7/Ofmfhin7k/K61q3XNoad1rAPGwECU="
|
||||||
|
|
||||||
|
aes = AESPKCS7(AES_KEY_SDGB_1_40, AES_IV_SDGB_1_40)
|
||||||
|
|
||||||
|
# 首先解码 base64
|
||||||
|
decodedData = base64.b64decode(base64_encoded_data)
|
||||||
|
# 然后解压数据,CN 2024 是加密后再压缩(纯傻逼
|
||||||
|
decompressedData = zlib.decompress(decodedData)
|
||||||
|
# 最后解密数据
|
||||||
|
decryptedData = aes.decrypt(decompressedData)
|
||||||
|
|
||||||
|
print(str(decryptedData))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main_sdga()
|
||||||
|
main_sdgb140()
|
||||||
78
backend/Standalone/DummyAimeDBServer.py
Normal file
78
backend/Standalone/DummyAimeDBServer.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# 舞萌DX AimeDB 服务器模拟实现
|
||||||
|
# 适用于舞萌DX 2024
|
||||||
|
# 理论可用于 HDD 登号等(这种情况下自行修改 hosts
|
||||||
|
|
||||||
|
# SGWCMAID111111111111AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
# 0 返回本地生成的假结果
|
||||||
|
# 1 原样返回官方服务器的结果
|
||||||
|
useOfficialServer = 0
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
# 解析命令行参数,作为用户 ID
|
||||||
|
import argparse
|
||||||
|
try:
|
||||||
|
parser = argparse.ArgumentParser(description="舞萌DX AimeDB 服务器模拟实现")
|
||||||
|
#parser.add_argument("userId", type=int, help="用户 ID")
|
||||||
|
#args = parser.parse_args()
|
||||||
|
#DUMMY_USER_ID = args.userId
|
||||||
|
raise Exception("未传入用户 ID")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"{e},未传入用户 ID,使用默认值")
|
||||||
|
DUMMY_USER_ID = 1
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
FastAPI,
|
||||||
|
Request
|
||||||
|
)
|
||||||
|
from fastapi.responses import (
|
||||||
|
PlainTextResponse,
|
||||||
|
JSONResponse
|
||||||
|
)
|
||||||
|
import rapidjson as json
|
||||||
|
import uvicorn
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 将当前目录的父目录加入到 sys.path 中
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
current_dir = Path(__file__).resolve().parent
|
||||||
|
parent_dir = current_dir.parent
|
||||||
|
sys.path.append(str(parent_dir))
|
||||||
|
|
||||||
|
from API_AimeDB import implAimeDB, calcSEGAAimeDBAuthKey
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post('/qrcode/api/alive_check')
|
||||||
|
async def qrcode_alive_check_api():
|
||||||
|
return PlainTextResponse('alive')
|
||||||
|
|
||||||
|
@app.post('/wc_aime/api/alive_check')
|
||||||
|
async def wc_aime_alive_check_api():
|
||||||
|
return PlainTextResponse('alive')
|
||||||
|
|
||||||
|
@app.post('/wc_aime/api/get_data')
|
||||||
|
async def get_data_dummy_api(request: Request):
|
||||||
|
gotRequest = json.loads((await request.body()).decode())
|
||||||
|
|
||||||
|
if useOfficialServer == 0:
|
||||||
|
fakeTimestamp = str(int(gotRequest['timestamp'])+3)
|
||||||
|
currentResponse = {
|
||||||
|
'errorID': 0,
|
||||||
|
'key': calcSEGAAimeDBAuthKey(str(DUMMY_USER_ID), fakeTimestamp),
|
||||||
|
'timestamp': fakeTimestamp,
|
||||||
|
'userID': DUMMY_USER_ID
|
||||||
|
}
|
||||||
|
logger.info(f"返回假结果: {currentResponse}")
|
||||||
|
return JSONResponse(currentResponse)
|
||||||
|
elif useOfficialServer == 1:
|
||||||
|
# 发给真正的 AimeDB
|
||||||
|
realAimeDBResponse = implAimeDB(gotRequest['qrCode'], True)
|
||||||
|
return JSONResponse(json.loads(realAimeDBResponse))
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=80)
|
||||||
69
backend/Standalone/DummyAuthLiteServer.py
Normal file
69
backend/Standalone/DummyAuthLiteServer.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 舞萌DX Auth-Lite 服务器模拟实现
|
||||||
|
# 仅实现了 /net/initialize 接口,用来处理 PowerOn
|
||||||
|
|
||||||
|
# NOT FINISHED: ONLY A DUMMY IMPLEMENTATION FOR TESTING
|
||||||
|
# Contact me if you have more information about this
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
FastAPI,
|
||||||
|
Request
|
||||||
|
)
|
||||||
|
from fastapi.responses import (
|
||||||
|
HTMLResponse
|
||||||
|
)
|
||||||
|
import uvicorn
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import pad, unpad
|
||||||
|
|
||||||
|
# 从 HDD 提取
|
||||||
|
LITE_AUTH_KEY = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
|
||||||
|
LITE_AUTH_IV = bytes.fromhex('00000000000000000000000000000000')
|
||||||
|
|
||||||
|
def auth_lite_encrypt(plaintext: str) -> bytes:
|
||||||
|
# 构造数据:16字节头 + 16字节0前缀 + 明文
|
||||||
|
header = bytes(16)
|
||||||
|
content = bytes(16) + plaintext.encode('utf-8')
|
||||||
|
data = header + content
|
||||||
|
# 填充并加密
|
||||||
|
padded_data = pad(data, AES.block_size)
|
||||||
|
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
|
||||||
|
return cipher.encrypt(padded_data)
|
||||||
|
|
||||||
|
def auth_lite_decrypt(ciphertext: bytes) -> str:
|
||||||
|
# 解密并去除填充
|
||||||
|
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
|
||||||
|
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||||
|
# 提取内容并解码
|
||||||
|
content = decrypted_data[16:] # 去除头部的16字节
|
||||||
|
return content.decode('utf-8').strip()
|
||||||
|
|
||||||
|
def apiOfficialServer(encryptedString: str):
|
||||||
|
url = "http://at.sys-allnet.cn/net/initialize"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"User-Agent": "SDGB;Windows/Lite"
|
||||||
|
}
|
||||||
|
data = encryptedString
|
||||||
|
response = httpx.post(url, headers=headers, data=data)
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
USE_OFFICIAL_SERVER = 1
|
||||||
|
|
||||||
|
@app.post('/net/initialize')
|
||||||
|
async def get_data_dummy_api(request: Request):
|
||||||
|
gotRequest = (await request.body())
|
||||||
|
if USE_OFFICIAL_SERVER == 1:
|
||||||
|
decrypted = auth_lite_decrypt(gotRequest)
|
||||||
|
officialResponse = apiOfficialServer(auth_lite_encrypt(decrypted))
|
||||||
|
logger.info(auth_lite_decrypt(officialResponse))
|
||||||
|
return HTMLResponse(officialResponse)
|
||||||
|
else:
|
||||||
|
# todo
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=80)
|
||||||
6
backend/Standalone/Script_GenerateLoginBonusDB.py
Normal file
6
backend/Standalone/Script_GenerateLoginBonusDB.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Never Gonna Give You Up
|
||||||
|
# Never Gonna Let You Down
|
||||||
|
# Never Gonna Run Around And Desert You
|
||||||
|
# Never Gonna Make You Cry
|
||||||
|
# Never Gonna Say Goodbye
|
||||||
|
# Never Gonna Tell A Lie And Hurt You
|
||||||
51
backend/Standalone/Script_GenerateMusicDB.py
Normal file
51
backend/Standalone/Script_GenerateMusicDB.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 感谢伟大的 Diving-Fish 让我被迫直面恐惧
|
||||||
|
import xml.dom.minidom as minidom
|
||||||
|
from pathlib import Path
|
||||||
|
import rapidjson as json
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
def makeMusicDBJson():
|
||||||
|
'''
|
||||||
|
从 HDD 的文件来生成 music_db.json
|
||||||
|
推荐的是如果要国服用 那就用国际服的文件来生成
|
||||||
|
免得国服每次更新还要重新生成太麻烦
|
||||||
|
'''
|
||||||
|
# 记得改
|
||||||
|
A000_DIR = Path('H:\PRiSM\Package\Sinmai_Data\StreamingAssets\A000')
|
||||||
|
OPTION_DIR = Path('H:\PRiSM\Package\Sinmai_Data\StreamingAssets')
|
||||||
|
|
||||||
|
music_db: dict[str, dict[str, str | int]] = {}
|
||||||
|
DEST_PATH = Path('../Data/musicDB.json')
|
||||||
|
|
||||||
|
music_folders = [f for f in (A000_DIR / 'music').iterdir() if f.is_dir()]
|
||||||
|
for option_dir in OPTION_DIR.iterdir():
|
||||||
|
if (option_dir / 'music').exists():
|
||||||
|
music_folders.extend([f for f in (option_dir / 'music').iterdir() if f.is_dir()])
|
||||||
|
|
||||||
|
for folder in music_folders:
|
||||||
|
xml_path = (folder / 'Music.xml')
|
||||||
|
if xml_path.exists():
|
||||||
|
xml = minidom.parse(xml_path.as_posix())
|
||||||
|
data = xml.getElementsByTagName('MusicData')[0]
|
||||||
|
music_id = data.getElementsByTagName('name')[0].getElementsByTagName('id')[0].firstChild.data
|
||||||
|
music_name = data.getElementsByTagName('name')[0].getElementsByTagName('str')[0].firstChild.data
|
||||||
|
music_version = data.getElementsByTagName('AddVersion')[0].getElementsByTagName('id')[0].firstChild.data
|
||||||
|
music_db[music_id] = {
|
||||||
|
"name": music_name,
|
||||||
|
"version": int(music_version)
|
||||||
|
}
|
||||||
|
logger.debug(f'Found {len(music_db)} music data')
|
||||||
|
serialized = '{\n'
|
||||||
|
sorted_keys = sorted(music_db.keys(), key=lambda x: int(x))
|
||||||
|
for key in sorted_keys:
|
||||||
|
value = music_db[key]
|
||||||
|
serialized += f' "{key}": {json.dumps(value, ensure_ascii=False)},\n'
|
||||||
|
serialized = serialized[:-2] + '\n}'
|
||||||
|
|
||||||
|
with open(DEST_PATH, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(serialized)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
makeMusicDBJson()
|
||||||
|
print('Done.')
|
||||||
113
backend/Standalone/UI.py
Normal file
113
backend/Standalone/UI.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import sys
|
||||||
|
import rapidjson as json
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QMainWindow,
|
||||||
|
QWidget,
|
||||||
|
QVBoxLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
QLabel,
|
||||||
|
QHBoxLayout,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
# 将当前目录的父目录加入到 sys.path 中
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
current_dir = Path(__file__).resolve().parent
|
||||||
|
parent_dir = current_dir.parent
|
||||||
|
sys.path.append(str(parent_dir))
|
||||||
|
|
||||||
|
from API_TitleServer import *
|
||||||
|
|
||||||
|
|
||||||
|
def sendRequest(requestText: str, apiNameText: str, uid: int) -> str:
|
||||||
|
try:
|
||||||
|
data = json.loads(requestText)
|
||||||
|
data = json.dumps(data)
|
||||||
|
except Exception:
|
||||||
|
return "给出的输入不是有效的 JSON"
|
||||||
|
try:
|
||||||
|
result = apiSDGB(data, apiNameText, uid)
|
||||||
|
except Exception as e:
|
||||||
|
return "请求失败:" + str(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTester(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# 主窗口设定
|
||||||
|
self.setWindowTitle("舞萌DX 2024 API 测试器")
|
||||||
|
self.resize(640, 400)
|
||||||
|
# 布局
|
||||||
|
mainWidget = QWidget()
|
||||||
|
self.setCentralWidget(mainWidget)
|
||||||
|
MainLayout = QVBoxLayout(mainWidget)
|
||||||
|
|
||||||
|
# 目标 API 输入框布局
|
||||||
|
TargetAPILayout = QHBoxLayout()
|
||||||
|
# API 输入框
|
||||||
|
self.TargetAPIInputBox = QLineEdit()
|
||||||
|
self.TargetAPIInputBox.setPlaceholderText("指定 API")
|
||||||
|
TargetAPILayout.addWidget(self.TargetAPIInputBox)
|
||||||
|
# API 后缀标签
|
||||||
|
TargetAPILabel = QLabel("MaimaiChn")
|
||||||
|
TargetAPILayout.addWidget(TargetAPILabel)
|
||||||
|
# 添加到主布局
|
||||||
|
MainLayout.addLayout(TargetAPILayout)
|
||||||
|
|
||||||
|
# UA额外信息输入框
|
||||||
|
self.AgentExtraInputBox = QLineEdit()
|
||||||
|
self.AgentExtraInputBox.setPlaceholderText("指定附加信息(UID或狗号)")
|
||||||
|
MainLayout.addWidget(self.AgentExtraInputBox)
|
||||||
|
|
||||||
|
# 请求输入框
|
||||||
|
self.RequestInputBox = QTextEdit()
|
||||||
|
self.RequestInputBox.setPlaceholderText("此处填入请求")
|
||||||
|
MainLayout.addWidget(self.RequestInputBox)
|
||||||
|
# 发送按钮
|
||||||
|
SendRequestButton = QPushButton("发送!")
|
||||||
|
SendRequestButton.clicked.connect(self.prepareRequest)
|
||||||
|
MainLayout.addWidget(SendRequestButton)
|
||||||
|
# 响应输出框
|
||||||
|
self.ResponseTextBox = QTextEdit()
|
||||||
|
self.ResponseTextBox.setPlaceholderText("此处显示输出")
|
||||||
|
self.ResponseTextBox.setReadOnly(True)
|
||||||
|
MainLayout.addWidget(self.ResponseTextBox)
|
||||||
|
|
||||||
|
# 布局设定
|
||||||
|
MainLayout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
MainLayout.setSpacing(5)
|
||||||
|
MainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
def prepareRequest(self):
|
||||||
|
# 发送请求用
|
||||||
|
try:
|
||||||
|
RequestDataString = self.RequestInputBox.toPlainText()
|
||||||
|
TargetAPIString = self.TargetAPIInputBox.text()
|
||||||
|
AgentExtraString = int(self.AgentExtraInputBox.text())
|
||||||
|
except Exception:
|
||||||
|
self.ResponseTextBox.setPlainText("输入无效")
|
||||||
|
return
|
||||||
|
|
||||||
|
Result = sendRequest(RequestDataString, TargetAPIString, AgentExtraString)
|
||||||
|
|
||||||
|
# 显示出输出
|
||||||
|
self.ResponseTextBox.setPlainText(Result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
# Set proper style for each OS
|
||||||
|
# if sys.platform == "win32":
|
||||||
|
# app.setStyle("windowsvista")
|
||||||
|
# else:
|
||||||
|
# app.setStyle("Fusion")
|
||||||
|
window = ApiTester()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
55
backend/_Special.py
Normal file
55
backend/_Special.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 纯纯测试用
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
|
||||||
|
from HelperFullPlay import implFullPlayAction, generateMusicData
|
||||||
|
|
||||||
|
from MyConfig import testUid8
|
||||||
|
|
||||||
|
|
||||||
|
def implChangeVersionNumber(
|
||||||
|
userId: int,
|
||||||
|
currentLoginTimestamp: int,
|
||||||
|
currentLoginResult,
|
||||||
|
dataVersion="1.40.09",
|
||||||
|
romVersion="1.41.00",
|
||||||
|
) -> str:
|
||||||
|
musicData = generateMusicData()
|
||||||
|
userAllPatches = {
|
||||||
|
"upsertUserAll": {
|
||||||
|
"userData": [
|
||||||
|
{
|
||||||
|
"lastRomVersion": romVersion,
|
||||||
|
"lastDataVersion": dataVersion,
|
||||||
|
"playerRating": 114514,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userMusicDetailList": [musicData],
|
||||||
|
"isNewMusicDetailList": "1", # 1避免覆盖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = implFullPlayAction(
|
||||||
|
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userId = testUid8
|
||||||
|
currentLoginTimestamp = generateTimestamp()
|
||||||
|
loginResult = apiLogin(currentLoginTimestamp, userId)
|
||||||
|
|
||||||
|
if loginResult["returnCode"] != 1:
|
||||||
|
logger.info("登录失败")
|
||||||
|
exit()
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
implChangeVersionNumber(
|
||||||
|
userId, currentLoginTimestamp, loginResult, "1.00.00", "1.00.00"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
finally:
|
||||||
|
logger.info(apiLogout(currentLoginTimestamp, userId))
|
||||||
|
# logger.warning("Error")
|
||||||
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)}
|
||||||
15
backend/pyproject.toml
Normal file
15
backend/pyproject.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[project]
|
||||||
|
name = "maimaidx-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"datetime>=5.5",
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"loguru>=0.7.3",
|
||||||
|
"pycryptodome>=3.23.0",
|
||||||
|
"python-rapidjson>=1.21",
|
||||||
|
"pytz>=2025.2",
|
||||||
|
"requests>=2.32.4",
|
||||||
|
]
|
||||||
7
backend/requirements.txt
Normal file
7
backend/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
datetime>=5.5
|
||||||
|
httpx>=0.28.1
|
||||||
|
loguru>=0.7.3
|
||||||
|
pycryptodome>=3.23.0
|
||||||
|
python-rapidjson>=1.21
|
||||||
|
pytz>=2025.2
|
||||||
|
requests>=2.32.4
|
||||||
317
backend/uv.lock
generated
Normal file
317
backend/uv.lock
generated
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.7.14"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "datetime"
|
||||||
|
version = "5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "zope-interface" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/66/e284b9978fede35185e5d18fb3ae855b8f573d8c90a56de5f6d03e8ef99e/DateTime-5.5.tar.gz", hash = "sha256:21ec6331f87a7fcb57bd7c59e8a68bfffe6fcbf5acdbbc7b356d6a9a020191d3", size = 63671, upload-time = "2024-03-21T07:26:50.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/78/8e382b8cb4346119e2e04270b6eb4a01c5ee70b47a8a0244ecdb157204f7/DateTime-5.5-py3-none-any.whl", hash = "sha256:0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120", size = 52649, upload-time = "2024-03-21T07:26:47.849Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loguru"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maimaidx-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "datetime" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "loguru" },
|
||||||
|
{ name = "pycryptodome" },
|
||||||
|
{ name = "python-rapidjson" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "datetime", specifier = ">=5.5" },
|
||||||
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
|
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||||
|
{ name = "python-rapidjson", specifier = ">=1.21" },
|
||||||
|
{ name = "pytz", specifier = ">=2025.2" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.4" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycryptodome"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-rapidjson"
|
||||||
|
version = "1.21"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cf/da/f041c85e7852ddb87dd59e34fdbfebf27defc6e4dfecbc0486ab30553dfd/python_rapidjson-1.21.tar.gz", hash = "sha256:4d0dd9cf1fcb6f4bf79ee606e6e9be4cfa598f273b91338a6974b6a99309a1e6", size = 238903, upload-time = "2025-07-10T06:30:43.413Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/c8/52735f227a9c662df8e4292af2b13960f0351795b0115f9fe95b3bfcfe62/python_rapidjson-1.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:354ac34828a242eec1896901fabe59d14adab7a5054d09a8bdaed4c6ee510b22", size = 220850, upload-time = "2025-07-10T07:19:00.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/83/f02aa1c9ea2594f05dd8c6c51e2ecea398a462118c7f4ddc004a135096f0/python_rapidjson-1.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ddd846b095fad357e4e4c5071e874f9c100c2e5b9765abcc7d71254820251c20", size = 210311, upload-time = "2025-07-10T07:19:02.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/60/b6a4026fd144fe80db1d6f7cb78191ed2f27fb46c69c719b3b63a8542072/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7faf4211354c1db9b21934d723f3c7db1d611ec3013458dc2fe1a732aa100d3e", size = 1679351, upload-time = "2025-07-10T07:19:04.419Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/7e/73ca21eb527fbe27b267ea04b9047fdfb4a524b2ce2565a082d84cdee7e9/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46ea95e9292260fd8174966eca05fad08b43ca0f5df1ccfab9796edb0868d8eb", size = 1722281, upload-time = "2025-07-10T07:19:06.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/9c/23da2d95b0b4de46695b57a593763ddb8e7f8a786c2793be89fc3dad957e/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1cac86399cfbaf24e72aef6eed1114180a72ba0242c7153f89d874a81fb83c6", size = 1719283, upload-time = "2025-07-10T07:19:07.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c1/52de9e4c137ccc98116c16835d9a708436d58ba51ba448e238ce08938d5b/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a4afbf54eb60dc56d4c2b6fac5d24709e8a5928baaeff0669c00950961c7a65", size = 2530062, upload-time = "2025-07-10T07:19:09.302Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/16/08d24136b5895157f1fdc75669796283d5116336c0b1c27fa73de260980f/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e8d4d1285c697325ad1b5300866176d190625bbdd85dce963300fad5bfaf166", size = 2635763, upload-time = "2025-07-10T07:19:10.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/a3/08aecff00e26e5dbb8cc8e5b2372b6f9ada28fcf09623e6de62f08ed2b99/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1525778bc9b9cee9b8f3058e0435f0e4ff03e2d8c3883893ebcaf58680316964", size = 2635419, upload-time = "2025-07-10T07:19:12.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/21/bd4e0cda3eada4d99562c1b5bcd2cb54588df17d63ab95b8b76af3d93826/python_rapidjson-1.21-cp312-cp312-win32.whl", hash = "sha256:b5717ddb9788ca00e0f97546cbdd3cfd0c25e52ab3bfed0951c7898e7f37cc60", size = 128528, upload-time = "2025-07-10T07:19:14.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/91/70f9083930de93e7ce9ccebd8bd598a5737829452c03c258ea862146bbfa/python_rapidjson-1.21-cp312-cp312-win_amd64.whl", hash = "sha256:409704e52ad25df661265dbcb3a512a2146c98f72f362adc157e9b595704e1be", size = 148421, upload-time = "2025-07-10T07:19:16.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/82/9f1ffd805d6c4b2329046f88b59eec077ea5b2a1894ec23f8fcf22f53e0f/python_rapidjson-1.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:863208f50b848b8e10aefcf0da2a67ce38a1044a2ebf83d6355f16f0c7865d4f", size = 220853, upload-time = "2025-07-10T07:19:17.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/d8/9de439a33cef3d1a7672028abcc1a3c8d0c9982f03a73b9cc71055325ee3/python_rapidjson-1.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:608ade2fff2788821f25e70af4b1bfbf9ab2b2dc8ad47c91616935c5d31e3b72", size = 210314, upload-time = "2025-07-10T07:19:19.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/93/dbee2d13a1619747652b8c8a921cb8a2e7309c0a142867b4954341c76632/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf6377c4e9636e20b403776585903d1ff52613a419c5f8453032146f0422634", size = 1679359, upload-time = "2025-07-10T07:19:21.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/56/06535489e561f6c2a035a5fa8d3d2ecc97c647fb4e1625a8cb7a1db46e6b/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74cba817cd9b16a3a54e7ddb3b59980c338c9a28900124502215dcba5db5a276", size = 1722183, upload-time = "2025-07-10T07:19:23.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/0b/7c27a2473edfb9d40eb65e5a225dc2e833a5e355f228502aa19c2bdf6679/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3194bde069c3f03f3c425de7037cca37d337b6c7ac30f42cd2f17e15f1c4da3a", size = 1718857, upload-time = "2025-07-10T07:19:24.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/0a/0db35ac49212505c347cdc353ddebd2b4085bfc02090baae552e7b5908b4/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b663932dc7548567028fb735bf4092b50dc373822dc9756a9ade6513541013de", size = 2530213, upload-time = "2025-07-10T07:19:26.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/6c/4972fb83f0419654d3083e0b4881f607cbf0829d64da43b7d2a3a21480a0/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fcb094ecca3d892f1dbd318251579ef66e2b2f06a2a24414ad71c31931d7ffff", size = 2636100, upload-time = "2025-07-10T07:19:27.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/08/da452b2bd67f88b8232e48f2341f3320ce9315f0bdfed1ca769b0b389acf/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f4cdb4f77b08b7ae121b7ca27ed2d57dcce17f8cd9f1d4593c954c982aba18ba", size = 2635274, upload-time = "2025-07-10T07:19:29.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/2b/b81e89c01ec25078a2326051b691443c90797da0d21e1b6c862128de5238/python_rapidjson-1.21-cp313-cp313-win32.whl", hash = "sha256:be63c0ef87bf26059ee77f5de21d84a1be3659cb6baa8484c237ffe17a8beb56", size = 128530, upload-time = "2025-07-10T07:19:30.743Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/03/e92189d6142860f949b9679c915a6a3752ccc2345a5f80a3995233c45f9f/python_rapidjson-1.21-cp313-cp313-win_amd64.whl", hash = "sha256:38307a2ef4fddbdc18806a796201fe668c30e60de7cd795943a3f7fd39535c8b", size = 148424, upload-time = "2025-07-10T07:19:32.212Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "80.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.14.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "win32-setctime"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zope-interface"
|
||||||
|
version = "7.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959, upload-time = "2024-11-28T08:47:47.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357, upload-time = "2024-11-28T08:47:50.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235, upload-time = "2024-11-28T09:18:15.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253, upload-time = "2024-11-28T08:48:29.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702, upload-time = "2024-11-28T08:48:37.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961, upload-time = "2024-11-28T08:48:29.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356, upload-time = "2024-11-28T08:48:33.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196, upload-time = "2024-11-28T09:18:17.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237, upload-time = "2024-11-28T08:48:31.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696, upload-time = "2024-11-28T08:48:41.161Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472, upload-time = "2024-11-28T08:49:56.587Z" },
|
||||||
|
]
|
||||||
8
frontend/.editorconfig
Normal file
8
frontend/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
1
frontend/.gitattributes
vendored
Normal file
1
frontend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
30
frontend/.gitignore
vendored
Normal file
30
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
6
frontend/.prettierrc.json
Normal file
6
frontend/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
39
frontend/README.md
Normal file
39
frontend/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# temp-vue-project
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
22
frontend/eslint.config.ts
Normal file
22
frontend/eslint.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>maimaiDX-API web tools</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5707
frontend/package-lock.json
generated
Normal file
5707
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
frontend/package.json
Normal file
43
frontend/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "temp-vue-project",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"element-plus": "^2.11.2",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"vue": "^3.5.18",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node22": "^22.0.2",
|
||||||
|
"@types/node": "^22.16.5",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"eslint": "^9.31.0",
|
||||||
|
"eslint-plugin-vue": "~10.3.0",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"vite": "^7.0.6",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.0",
|
||||||
|
"vue-tsc": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
105
frontend/src/App.vue
Normal file
105
frontend/src/App.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="main-layout">
|
||||||
|
<el-aside :width="isCollapsed ? '64px' : '200px'" class="main-aside">
|
||||||
|
<el-menu
|
||||||
|
default-active="/profile"
|
||||||
|
class="el-menu-vertical-demo"
|
||||||
|
:collapse="isCollapsed"
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<div class="header-container">
|
||||||
|
<span v-if="!isCollapsed">SDGB API TOOLS</span>
|
||||||
|
</div>
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><HomeFilled /></el-icon>
|
||||||
|
<template #title>首页</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/music">
|
||||||
|
<el-icon><Headset /></el-icon>
|
||||||
|
<template #title>乐曲列表</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/profile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<template #title>用户中心</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/aime-db">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
<template #title>Aime卡扫描</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/auth-lite-delivery">
|
||||||
|
<el-icon><Link /></el-icon>
|
||||||
|
<template #title>更新链接</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/logs">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<template #title>系统日志</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="main-header">
|
||||||
|
<el-icon @click="isCollapsed = !isCollapsed" class="collapse-icon">
|
||||||
|
<component :is="isCollapsed ? 'Expand' : 'Fold'" />
|
||||||
|
</el-icon>
|
||||||
|
<span>SDGB 1.51</span>
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { HomeFilled, Headset, User, Fold, Expand, Link, CreditCard, Document } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const isCollapsed = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-layout {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-aside {
|
||||||
|
transition: width 0.3s;
|
||||||
|
background-color: #fff;
|
||||||
|
border-right: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-main {
|
||||||
|
background-color: #f4f7fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
10
frontend/src/assets/main.css
Normal file
10
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
41
frontend/src/components/HelloWorld.vue
Normal file
41
frontend/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
msg: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
136
frontend/src/components/LogViewer.vue
Normal file
136
frontend/src/components/LogViewer.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import logger, { getLogBuffer } from '@/utils/logger'
|
||||||
|
|
||||||
|
const logs = ref<any[]>(getLogBuffer())
|
||||||
|
const filterLevel = ref('ALL')
|
||||||
|
const filterText = ref('')
|
||||||
|
|
||||||
|
// 过滤后的日志
|
||||||
|
const filteredLogs = computed(() => {
|
||||||
|
let filtered = logs.value
|
||||||
|
|
||||||
|
// 按级别过滤
|
||||||
|
if (filterLevel.value !== 'ALL') {
|
||||||
|
filtered = filtered.filter(log => log.level === filterLevel.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按文本过滤
|
||||||
|
if (filterText.value) {
|
||||||
|
const searchText = filterText.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(log =>
|
||||||
|
log.message.toLowerCase().includes(searchText) ||
|
||||||
|
(log.data && JSON.stringify(log.data).toLowerCase().includes(searchText))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序排列
|
||||||
|
return filtered.slice().reverse()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 刷新日志
|
||||||
|
function refreshLogs() {
|
||||||
|
logs.value = getLogBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
function clearLogs() {
|
||||||
|
logger.clearLogBuffer()
|
||||||
|
logs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志级别对应的标签类型
|
||||||
|
function getTagType(level: string) {
|
||||||
|
switch (level) {
|
||||||
|
case 'ERROR': return 'danger'
|
||||||
|
case 'WARN': return 'warning'
|
||||||
|
case 'INFO': return 'success'
|
||||||
|
case 'DEBUG': return 'info'
|
||||||
|
default: return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
|
function formatTimestamp(timestamp: string) {
|
||||||
|
return new Date(timestamp).toLocaleString()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="log-viewer">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-input
|
||||||
|
v-model="filterText"
|
||||||
|
placeholder="搜索日志..."
|
||||||
|
clearable
|
||||||
|
style="width: 200px; margin-right: 10px;"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-model="filterLevel"
|
||||||
|
placeholder="选择级别"
|
||||||
|
style="width: 120px; margin-right: 10px;"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="ALL" />
|
||||||
|
<el-option label="DEBUG" value="DEBUG" />
|
||||||
|
<el-option label="INFO" value="INFO" />
|
||||||
|
<el-option label="WARN" value="WARN" />
|
||||||
|
<el-option label="ERROR" value="ERROR" />
|
||||||
|
</el-select>
|
||||||
|
<el-button @click="refreshLogs">刷新</el-button>
|
||||||
|
<el-button @click="clearLogs">清空</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filteredLogs" stripe style="width: 100%" height="400">
|
||||||
|
<el-table-column prop="timestamp" label="时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatTimestamp(scope.row.timestamp) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="level" label="级别" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getTagType(scope.row.level)">
|
||||||
|
{{ scope.row.level }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="message" label="消息" />
|
||||||
|
<el-table-column label="数据" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-popover
|
||||||
|
v-if="scope.row.data"
|
||||||
|
placement="left"
|
||||||
|
:width="300"
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="primary" link>查看</el-button>
|
||||||
|
</template>
|
||||||
|
<pre>{{ JSON.stringify(scope.row.data, null, 2) }}</pre>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.log-viewer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
frontend/src/components/TheWelcome.vue
Normal file
94
frontend/src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
|
||||||
|
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||||
|
you need to test your components and web pages, check out
|
||||||
|
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||||
|
and
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||||
|
/
|
||||||
|
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in
|
||||||
|
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||||
|
>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||||
|
(our official Discord server), or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also follow the official
|
||||||
|
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||||
|
Bluesky account or the
|
||||||
|
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
X account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
87
frontend/src/components/WelcomeItem.vue
Normal file
87
frontend/src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
frontend/src/components/icons/IconCommunity.vue
Normal file
7
frontend/src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend/src/components/icons/IconDocumentation.vue
Normal file
7
frontend/src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend/src/components/icons/IconEcosystem.vue
Normal file
7
frontend/src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend/src/components/icons/IconSupport.vue
Normal file
7
frontend/src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
frontend/src/components/icons/IconTooling.vue
Normal file
19
frontend/src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
20
frontend/src/main.ts
Normal file
20
frontend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
45
frontend/src/router/index.ts
Normal file
45
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
import MusicView from '../views/MusicView.vue'
|
||||||
|
import ProfileView from '../views/ProfileView.vue'
|
||||||
|
import AuthLiteDeliveryView from '../views/AuthLiteDeliveryView.vue'
|
||||||
|
import AimeDBView from '../views/AimeDBView.vue'
|
||||||
|
import LogsView from '../views/LogsView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music',
|
||||||
|
name: 'music',
|
||||||
|
component: MusicView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'profile',
|
||||||
|
component: ProfileView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/aime-db',
|
||||||
|
name: 'aime-db',
|
||||||
|
component: AimeDBView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth-lite-delivery',
|
||||||
|
name: 'auth-lite-delivery',
|
||||||
|
component: AuthLiteDeliveryView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logs',
|
||||||
|
name: 'logs',
|
||||||
|
component: LogsView
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
143
frontend/src/utils/logger.ts
Normal file
143
frontend/src/utils/logger.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// 日志级别枚举
|
||||||
|
const LogLevel = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前日志级别
|
||||||
|
let currentLogLevel = LogLevel.INFO;
|
||||||
|
|
||||||
|
// 日志缓冲区
|
||||||
|
let logBuffer = [];
|
||||||
|
const MAX_BUFFER_SIZE = 1000;
|
||||||
|
|
||||||
|
// 设置日志级别
|
||||||
|
export function setLogLevel(level) {
|
||||||
|
currentLogLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间戳
|
||||||
|
function getTimestamp() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入日志到缓冲区
|
||||||
|
function writeToBuffer(level, message, data = null) {
|
||||||
|
const logEntry = {
|
||||||
|
timestamp: getTimestamp(),
|
||||||
|
level: Object.keys(LogLevel).find(key => LogLevel[key] === level),
|
||||||
|
message: message,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
logBuffer.push(logEntry);
|
||||||
|
|
||||||
|
// 保持缓冲区大小在限制内
|
||||||
|
if (logBuffer.length > MAX_BUFFER_SIZE) {
|
||||||
|
logBuffer = logBuffer.slice(-MAX_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在控制台输出日志
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.DEBUG:
|
||||||
|
console.debug(`[DEBUG] ${message}`, data);
|
||||||
|
break;
|
||||||
|
case LogLevel.INFO:
|
||||||
|
console.info(`[INFO] ${message}`, data);
|
||||||
|
break;
|
||||||
|
case LogLevel.WARN:
|
||||||
|
console.warn(`[WARN] ${message}`, data);
|
||||||
|
break;
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
console.error(`[ERROR] ${message}`, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug级别日志
|
||||||
|
export function logDebug(message, data = null) {
|
||||||
|
if (currentLogLevel <= LogLevel.DEBUG) {
|
||||||
|
writeToBuffer(LogLevel.DEBUG, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info级别日志
|
||||||
|
export function logInfo(message, data = null) {
|
||||||
|
if (currentLogLevel <= LogLevel.INFO) {
|
||||||
|
writeToBuffer(LogLevel.INFO, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn级别日志
|
||||||
|
export function logWarn(message, data = null) {
|
||||||
|
if (currentLogLevel <= LogLevel.WARN) {
|
||||||
|
writeToBuffer(LogLevel.WARN, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error级别日志
|
||||||
|
export function logError(message, data = null) {
|
||||||
|
if (currentLogLevel <= LogLevel.ERROR) {
|
||||||
|
writeToBuffer(LogLevel.ERROR, message, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志缓冲区
|
||||||
|
export function getLogBuffer() {
|
||||||
|
return [...logBuffer]; // 返回副本
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空日志缓冲区
|
||||||
|
export function clearLogBuffer() {
|
||||||
|
logBuffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将日志保存到文件
|
||||||
|
export function saveLogsToFile() {
|
||||||
|
const logs = getLogBuffer();
|
||||||
|
const logText = logs.map(entry =>
|
||||||
|
`${entry.timestamp} [${entry.level}] ${entry.message} ${entry.data ? JSON.stringify(entry.data) : ''}`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([logText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `frontend-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API请求日志
|
||||||
|
export function logApiRequest(method, url, data = null) {
|
||||||
|
logInfo(`API Request: ${method} ${url}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应日志
|
||||||
|
export function logApiResponse(status, data = null) {
|
||||||
|
if (status >= 400) {
|
||||||
|
logError(`API Response: ${status}`, data);
|
||||||
|
} else {
|
||||||
|
logInfo(`API Response: ${status}`, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
LogLevel,
|
||||||
|
setLogLevel,
|
||||||
|
logDebug,
|
||||||
|
logInfo,
|
||||||
|
logWarn,
|
||||||
|
logError,
|
||||||
|
getLogBuffer,
|
||||||
|
clearLogBuffer,
|
||||||
|
saveLogsToFile,
|
||||||
|
logApiRequest,
|
||||||
|
logApiResponse
|
||||||
|
};
|
||||||
15
frontend/src/views/AboutView.vue
Normal file
15
frontend/src/views/AboutView.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about">
|
||||||
|
<h1>This is an about page</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.about {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
frontend/src/views/AimeDBView.vue
Normal file
110
frontend/src/views/AimeDBView.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const qrContent = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const scanResult = ref<any>(null)
|
||||||
|
|
||||||
|
const apiBaseUrl = computed(() => {
|
||||||
|
return `http://${window.location.hostname}:8000/api`
|
||||||
|
})
|
||||||
|
|
||||||
|
async function scanQRCode() {
|
||||||
|
if (!qrContent.value) {
|
||||||
|
ElMessage.error('请输入二维码内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
scanResult.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/aime_scan`, { qrContent: qrContent.value })
|
||||||
|
scanResult.value = data
|
||||||
|
if (data.errorID) {
|
||||||
|
ElMessage.error(`扫描失败: 错误代码 ${data.errorID}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('二维码扫描成功')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = e.response?.data?.detail || e.message
|
||||||
|
ElMessage.error(`扫描失败: ${message}`)
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearResult() {
|
||||||
|
scanResult.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="aime-db-view">
|
||||||
|
<el-card header="AimeDB 二维码扫描" style="width: 100%;">
|
||||||
|
<el-form :inline="true" @submit.prevent="scanQRCode">
|
||||||
|
<el-form-item label="二维码内容">
|
||||||
|
<el-input
|
||||||
|
v-model="qrContent"
|
||||||
|
placeholder="请输入Aime卡二维码内容"
|
||||||
|
clearable
|
||||||
|
style="width: 300px;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="scanQRCode"
|
||||||
|
:loading="isLoading"
|
||||||
|
>
|
||||||
|
扫描
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="clearResult">清空结果</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-card v-if="scanResult" header="扫描结果" style="margin-top: 20px;">
|
||||||
|
<div v-if="scanResult.errorID">
|
||||||
|
<el-alert
|
||||||
|
title="扫描失败"
|
||||||
|
:description="`错误代码: ${scanResult.errorID}`"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="用户ID">{{ scanResult.userID }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Aime卡ID">{{ scanResult.aimeID }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="访问代码">{{ scanResult.accessCode }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="芯片ID">{{ scanResult.chipID }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="返回代码">{{ scanResult.returnCode }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
<h3>原始数据</h3>
|
||||||
|
<pre>{{ JSON.stringify(scanResult, null, 2) }}</pre>
|
||||||
|
</el-card>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.aime-db-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
frontend/src/views/AuthLiteDeliveryView.vue
Normal file
105
frontend/src/views/AuthLiteDeliveryView.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const authLiteDeliveryData = ref<any>(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const parsedIniData = ref<any>(null) // New ref for parsed INI data
|
||||||
|
|
||||||
|
const apiBaseUrl = computed(() => {
|
||||||
|
return `http://${window.location.hostname}:8000/api`
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getAuthLiteDelivery() {
|
||||||
|
isLoading.value = true
|
||||||
|
parsedIniData.value = null; // Clear previous parsed data
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${apiBaseUrl.value}/get_auth_lite_delivery`)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
authLiteDeliveryData.value = data
|
||||||
|
ElMessage.success('已获取 AuthLiteDelivery 信息')
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = e.response?.data?.detail || e.message
|
||||||
|
ElMessage.error(`获取失败: ${message}`)
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseIni(url: string) {
|
||||||
|
isLoading.value = true
|
||||||
|
parsedIniData.value = null; // Clear previous parsed data
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/parse_update_ini`, { url })
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
parsedIniData.value = data.parsedData
|
||||||
|
ElMessage.success('INI 文件解析成功')
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = e.response?.data?.detail || e.message
|
||||||
|
ElMessage.error(`INI 文件解析失败: ${message}`)
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth-lite-delivery-view">
|
||||||
|
<el-card header="AuthLiteDelivery 信息获取" style="width: 100%;">
|
||||||
|
<el-form :inline="true" @submit.prevent="getAuthLiteDelivery">
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getAuthLiteDelivery" :loading="isLoading">获取 AuthLiteDelivery</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-card v-if="authLiteDeliveryData" header="AuthLiteDelivery 结果" style="margin-top: 20px;">
|
||||||
|
<p>更新链接列表:</p>
|
||||||
|
<el-space wrap>
|
||||||
|
<div v-for="(link, index) in authLiteDeliveryData.updateLinks" :key="index" class="link-item">
|
||||||
|
<el-link :href="link" target="_blank">{{ link }}</el-link>
|
||||||
|
<el-button type="text" @click="parseIni(link)" :loading="isLoading">解析</el-button>
|
||||||
|
</div>
|
||||||
|
</el-space>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="parsedIniData" header="解析后的 INI 内容" style="margin-top: 20px;">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="游戏描述">{{ parsedIniData.game_desc }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发布时间">{{ parsedIniData.release_time }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="主更新包">{{ parsedIniData.main_file }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="可选更新包">
|
||||||
|
<ul v-if="parsedIniData.optional_files && parsedIniData.optional_files.length">
|
||||||
|
<li v-for="(file, idx) in parsedIniData.optional_files" :key="idx">{{ file }}</li>
|
||||||
|
</ul>
|
||||||
|
<span v-else>无</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-divider />
|
||||||
|
<h3>原始解析数据 (完整)</h3>
|
||||||
|
<pre>{{ JSON.stringify(parsedIniData, null, 2) }}</pre>
|
||||||
|
</el-card>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-lite-delivery-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.link-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
frontend/src/views/HomeView.vue
Normal file
18
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<h1>舞萌DX 2025(SDGB 1.51)API网页工具</h1>
|
||||||
|
<p>本工具仅供学习交流使用,请勿用于非法用途</p>
|
||||||
|
<p>目前已经实现的功能:</p>
|
||||||
|
<p>用户所有信息查看(需要获取二维码)</p>
|
||||||
|
<p>用户信息预览(免获取二维码)</p>
|
||||||
|
<p>领取登录奖励等等</p>
|
||||||
|
<img src='https://i.mji.rip/2025/09/18/5bdc72c934da195de76f2df4677a26d3.png'></img>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
182
frontend/src/views/LogsView.vue
Normal file
182
frontend/src/views/LogsView.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
import logger, { getLogBuffer, saveLogsToFile } from '@/utils/logger'
|
||||||
|
|
||||||
|
const backendLogs = ref<any[]>([])
|
||||||
|
const frontendLogs = ref<any[]>(getLogBuffer())
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const autoRefresh = ref(false)
|
||||||
|
const refreshInterval = ref<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
const apiBaseUrl = `http://${window.location.hostname}:8000/api`
|
||||||
|
|
||||||
|
// 获取后端日志
|
||||||
|
async function fetchBackendLogs() {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
logger.logInfo('Fetching backend logs')
|
||||||
|
const { data } = await axios.get(`${apiBaseUrl}/logs`)
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error)
|
||||||
|
}
|
||||||
|
backendLogs.value = data.logs || []
|
||||||
|
ElMessage.success('后端日志获取成功')
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.logError('Failed to fetch backend logs', e)
|
||||||
|
ElMessage.error('获取后端日志失败: ' + (e.response?.data?.detail || e.message))
|
||||||
|
// 使用模拟数据
|
||||||
|
backendLogs.value = [
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'INFO', message: 'Backend started successfully' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'INFO', message: 'Aime scan request received' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'WARN', message: 'QR content validation failed' }
|
||||||
|
]
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新所有日志
|
||||||
|
function refreshLogs() {
|
||||||
|
frontendLogs.value = getLogBuffer()
|
||||||
|
fetchBackendLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始自动刷新
|
||||||
|
function startAutoRefresh() {
|
||||||
|
if (refreshInterval.value) {
|
||||||
|
clearInterval(refreshInterval.value)
|
||||||
|
}
|
||||||
|
refreshInterval.value = setInterval(() => {
|
||||||
|
refreshLogs()
|
||||||
|
}, 5000) // 每5秒刷新一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止自动刷新
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (refreshInterval.value) {
|
||||||
|
clearInterval(refreshInterval.value)
|
||||||
|
refreshInterval.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换自动刷新
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存日志到文件
|
||||||
|
function saveLogs() {
|
||||||
|
saveLogsToFile()
|
||||||
|
ElMessage.success('日志已保存到文件')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时的操作
|
||||||
|
onMounted(() => {
|
||||||
|
refreshLogs()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时的操作
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="logs-view">
|
||||||
|
<el-card header="系统日志" style="width: 100%;">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button type="primary" @click="refreshLogs" :loading="isLoading">刷新日志</el-button>
|
||||||
|
<el-checkbox v-model="autoRefresh" @change="toggleAutoRefresh">自动刷新</el-checkbox>
|
||||||
|
<el-button @click="clearFrontendLogs">清空前端日志</el-button>
|
||||||
|
<el-button @click="saveLogs">保存日志到文件</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tabs type="border-card">
|
||||||
|
<el-tab-pane label="前端日志">
|
||||||
|
<el-table :data="frontendLogs" stripe style="width: 100%" height="500">
|
||||||
|
<el-table-column prop="timestamp" label="时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ new Date(scope.row.timestamp).toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="level" label="级别" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.level === 'ERROR' ? 'danger' :
|
||||||
|
scope.row.level === 'WARN' ? 'warning' :
|
||||||
|
scope.row.level === 'INFO' ? 'success' : 'info'">
|
||||||
|
{{ scope.row.level }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="message" label="消息" />
|
||||||
|
<el-table-column prop="data" label="数据" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-popover
|
||||||
|
v-if="scope.row.data"
|
||||||
|
placement="left"
|
||||||
|
:width="300"
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small">查看数据</el-button>
|
||||||
|
</template>
|
||||||
|
<pre>{{ JSON.stringify(scope.row.data, null, 2) }}</pre>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="后端日志">
|
||||||
|
<el-table :data="backendLogs" stripe style="width: 100%" height="500">
|
||||||
|
<el-table-column prop="timestamp" label="时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ new Date(scope.row.timestamp).toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="level" label="级别" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.level === 'ERROR' ? 'danger' :
|
||||||
|
scope.row.level === 'WARN' ? 'warning' :
|
||||||
|
scope.row.level === 'INFO' ? 'success' : 'info'">
|
||||||
|
{{ scope.row.level }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="message" label="消息" />
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logs-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
frontend/src/views/MusicView.vue
Normal file
62
frontend/src/views/MusicView.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const musicList = ref<any[]>([])
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const searchTerm = ref('')
|
||||||
|
|
||||||
|
const apiBaseUrl = computed(() => `http://${window.location.hostname}:8000/api`)
|
||||||
|
|
||||||
|
const filteredMusic = computed(() => {
|
||||||
|
if (!searchTerm.value) {
|
||||||
|
return musicList.value
|
||||||
|
}
|
||||||
|
return musicList.value.filter(music =>
|
||||||
|
music.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${apiBaseUrl.value}/music`)
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('Invalid data format received from API.')
|
||||||
|
}
|
||||||
|
musicList.value = data
|
||||||
|
if (data.length === 0) {
|
||||||
|
ElMessage.warning('API returned empty music list.')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(`无法加载乐曲列表: ${e.message}`)
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="music-view">
|
||||||
|
<el-card header="乐曲列表">
|
||||||
|
<el-input
|
||||||
|
v-model="searchTerm"
|
||||||
|
placeholder="搜索歌曲名"
|
||||||
|
clearable
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
/>
|
||||||
|
<el-table :data="filteredMusic" stripe v-loading="isLoading" height="75vh" empty-text="没有数据">
|
||||||
|
<el-table-column prop="id" label="ID" width="100" sortable />
|
||||||
|
<el-table-column prop="name" label="标题" sortable />
|
||||||
|
<el-table-column prop="version" label="版本" width="120" sortable />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.music-view {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
397
frontend/src/views/ProfileView.vue
Normal file
397
frontend/src/views/ProfileView.vue
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import logger from '@/utils/logger'
|
||||||
|
|
||||||
|
// --- API Configuration ---
|
||||||
|
const apiBaseUrl = computed(() => {
|
||||||
|
return `http://${window.location.hostname}:8000/api`
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- State Management ---
|
||||||
|
const inputUserId = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const userData = ref<any>(null)
|
||||||
|
const previewData = ref<any>(null)
|
||||||
|
const musicResults = ref<any[] | null>(null)
|
||||||
|
const anyData = ref<any>(null)
|
||||||
|
|
||||||
|
// AimeDB Scan State
|
||||||
|
const qrContent = ref('')
|
||||||
|
const isScanning = ref(false)
|
||||||
|
|
||||||
|
// Action-specific state
|
||||||
|
const fishImportToken = ref('')
|
||||||
|
const itemKind = ref('')
|
||||||
|
const itemIds = ref('')
|
||||||
|
const ticketType = ref('')
|
||||||
|
const apiName = ref('')
|
||||||
|
const scoreForm = reactive({
|
||||||
|
musicId: '',
|
||||||
|
levelId: '',
|
||||||
|
achievement: '',
|
||||||
|
dxScore: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Generic Action Wrapper ---
|
||||||
|
async function performAction(action: () => Promise<void>, confirmOptions?: { title: string; message: string }) {
|
||||||
|
if (!inputUserId.value) {
|
||||||
|
ElMessage.error('请先输入用户ID');
|
||||||
|
logger.logWarn('Perform action failed: User ID is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmOptions) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(confirmOptions.message, confirmOptions.title, {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
ElMessage.info('操作已取消');
|
||||||
|
logger.logInfo('Action cancelled by user');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = e.response?.data?.detail || e.message;
|
||||||
|
logger.logError('Action failed', { error: message, stack: e.stack });
|
||||||
|
ElMessage.error(message);
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AimeDB Scan Function ---
|
||||||
|
async function scanAimeQR() {
|
||||||
|
if (!qrContent.value) {
|
||||||
|
ElMessage.error('请输入二维码内容')
|
||||||
|
logger.logWarn('Aime scan failed: QR content is empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logInfo('Starting Aime QR scan', { qrContent: qrContent.value.substring(0, 20) + '...' })
|
||||||
|
isScanning.value = true
|
||||||
|
try {
|
||||||
|
const requestData = { qrContent: qrContent.value }
|
||||||
|
logger.logApiRequest('POST', '/api/aime_scan', requestData)
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/aime_scan`, requestData)
|
||||||
|
logger.logApiResponse(200, data)
|
||||||
|
|
||||||
|
if (data.errorID) {
|
||||||
|
logger.logWarn('Aime scan failed', { errorID: data.errorID })
|
||||||
|
ElMessage.error(`扫描失败: 错误代码 ${data.errorID}`)
|
||||||
|
} else if (data.userID) {
|
||||||
|
inputUserId.value = data.userID
|
||||||
|
logger.logInfo('Aime scan successful', { userID: data.userID })
|
||||||
|
ElMessage.success('二维码扫描成功,用户ID已填充')
|
||||||
|
} else {
|
||||||
|
logger.logWarn('Aime scan returned no userID')
|
||||||
|
ElMessage.error('扫描成功但未返回有效用户ID')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.logError('Aime scan failed with exception', e)
|
||||||
|
const message = e.response?.data?.detail || e.message
|
||||||
|
ElMessage.error(`扫描失败: ${message}`)
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isScanning.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- API Functions ---
|
||||||
|
const fetchFullProfile = () => {
|
||||||
|
if (!inputUserId.value) {
|
||||||
|
ElMessage.error('请输入用户ID');
|
||||||
|
logger.logWarn('Fetch full profile failed: User ID is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.logInfo('Fetching full profile', { userId: inputUserId.value });
|
||||||
|
performAction(async () => {
|
||||||
|
const url = `${apiBaseUrl.value}/user/all?userId=${inputUserId.value}`;
|
||||||
|
logger.logApiRequest('GET', url);
|
||||||
|
const { data } = await axios.get(url);
|
||||||
|
logger.logApiResponse(200, data);
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
userData.value = data.upsertUserAll.userData[0];
|
||||||
|
previewData.value = null;
|
||||||
|
musicResults.value = null;
|
||||||
|
ElMessage.success('已加载完整用户信息');
|
||||||
|
logger.logInfo('Successfully loaded full profile', { userId: inputUserId.value });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPreview = () => {
|
||||||
|
if (!inputUserId.value) {
|
||||||
|
ElMessage.error('请输入用户ID');
|
||||||
|
logger.logWarn('Fetch preview failed: User ID is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.logInfo('Fetching user preview', { userId: inputUserId.value });
|
||||||
|
performAction(async () => {
|
||||||
|
const url = `${apiBaseUrl.value}/user/preview?userId=${inputUserId.value}`;
|
||||||
|
logger.logApiRequest('GET', url);
|
||||||
|
const { data } = await axios.get(url);
|
||||||
|
logger.logApiResponse(200, data);
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
previewData.value = data;
|
||||||
|
userData.value = null;
|
||||||
|
musicResults.value = null;
|
||||||
|
ElMessage.success('已加载用户预览信息');
|
||||||
|
logger.logInfo('Successfully loaded user preview', { userId: inputUserId.value });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchScores = () => {
|
||||||
|
logger.logInfo('Fetching user scores', { userId: inputUserId.value });
|
||||||
|
performAction(async () => {
|
||||||
|
const url = `${apiBaseUrl.value}/user/music_results?userId=${inputUserId.value}`;
|
||||||
|
logger.logApiRequest('GET', url);
|
||||||
|
const { data } = await axios.get(url);
|
||||||
|
logger.logApiResponse(200, data);
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
musicResults.value = data.userMusicResults;
|
||||||
|
ElMessage.success(`已找到 ${musicResults.value?.length || 0} 条分数记录`);
|
||||||
|
logger.logInfo('Successfully loaded user scores', { userId: inputUserId.value, count: musicResults.value?.length || 0 });
|
||||||
|
})};
|
||||||
|
|
||||||
|
// --- Action Functions ---
|
||||||
|
const claimBonus = () => {
|
||||||
|
logger.logInfo('Claiming login bonus', { userId: inputUserId.value });
|
||||||
|
performAction(async () => {
|
||||||
|
const url = `${apiBaseUrl.value}/action/claim_login_bonus`;
|
||||||
|
const requestData = { userId: inputUserId.value };
|
||||||
|
logger.logApiRequest('POST', url, requestData);
|
||||||
|
const { data } = await axios.post(url, requestData);
|
||||||
|
logger.logApiResponse(200, data);
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
ElMessage.success(data.message);
|
||||||
|
logger.logInfo('Successfully claimed login bonus', { userId: inputUserId.value });
|
||||||
|
})};
|
||||||
|
|
||||||
|
const doUnlockItem = () => {
|
||||||
|
logger.logInfo('Unlocking item', { userId: inputUserId.value, itemKind: itemKind.value, itemIds: itemIds.value });
|
||||||
|
performAction(async () => {
|
||||||
|
if (!itemKind.value || !itemIds.value) throw new Error('道具种类和ID不能为空');
|
||||||
|
const payload = { userId: inputUserId.value, itemKind: itemKind.value, itemIds: itemIds.value };
|
||||||
|
const url = `${apiBaseUrl.value}/action/unlock_item`;
|
||||||
|
logger.logApiRequest('POST', url, payload);
|
||||||
|
const { data } = await axios.post(url, payload);
|
||||||
|
logger.logApiResponse(200, data);
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
ElMessage.success(data.message);
|
||||||
|
logger.logInfo('Successfully unlocked item', { userId: inputUserId.value, itemKind: itemKind.value });
|
||||||
|
})};
|
||||||
|
|
||||||
|
const uploadToFish = () => performAction(async () => {
|
||||||
|
if (!fishImportToken.value) throw new Error('水鱼查分器导入Token不能为空')
|
||||||
|
const payload = { userId: inputUserId.value, fishImportToken: fishImportToken.value }
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/user/upload_to_diving_fish`, payload)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
const buyTicket = () => performAction(async () => {
|
||||||
|
if (!ticketType.value) throw new Error('票券类型不能为空')
|
||||||
|
const payload = { userId: inputUserId.value, ticketType: ticketType.value }
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/action/buy_ticket`, payload)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
const forceLogout = () => performAction(async () => {
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/action/force_logout`, { userId: inputUserId.value })
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.info(data.message)
|
||||||
|
}, { title: '确认强制登出', message: '这会尝试将用户从当前登录的机台上强制登出,确定吗?' })
|
||||||
|
|
||||||
|
const wipeTickets = () => performAction(async () => {
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/action/wipe_tickets`, { userId: inputUserId.value })
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
}, { title: '确认清空票券', message: '此操作会清空该用户的所有票券,确定吗?' })
|
||||||
|
|
||||||
|
const uploadScore = () => performAction(async () => {
|
||||||
|
const { musicId, levelId, achievement, dxScore } = scoreForm
|
||||||
|
if (!musicId || !levelId || !achievement || !dxScore) throw new Error('上传分数所需的所有字段均不能为空')
|
||||||
|
const payload = { ...scoreForm, userId: inputUserId.value }
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/action/upload_score`, payload)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteScore = () => performAction(async () => {
|
||||||
|
const { musicId, levelId } = scoreForm
|
||||||
|
if (!musicId || !levelId) throw new Error('删除分数需要乐曲ID和难度ID')
|
||||||
|
const payload = { musicId, levelId, userId: inputUserId.value }
|
||||||
|
const { data } = await axios.post(`${apiBaseUrl.value}/action/delete_score`, payload)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
}, { title: '确认删除分数', message: `确定要删除乐曲 ${scoreForm.musicId} (难度 ${scoreForm.levelId}) 的分数记录吗?` })
|
||||||
|
|
||||||
|
const getAny = () => performAction(async () => {
|
||||||
|
if (!apiName.value) throw new Error('API名称不能为空')
|
||||||
|
const { data } = await axios.get(`${apiBaseUrl.value}/get_any?userId=${inputUserId.value}&apiName=${apiName.value}`)
|
||||||
|
if (data.error) throw new Error(data.error)
|
||||||
|
anyData.value = data
|
||||||
|
ElMessage.success(`已成功调用 ${apiName.value}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-space direction="vertical" alignment="start" :size="20" style="width: 100%;">
|
||||||
|
<el-card header="用户选择" style="width: 100%;">
|
||||||
|
<el-form :inline="true" @submit.prevent="fetchFullProfile">
|
||||||
|
<el-form-item label="用户ID">
|
||||||
|
<el-input v-model="inputUserId" placeholder="请输入用户ID" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="fetchFullProfile" :loading="isLoading">获取完整信息</el-button>
|
||||||
|
<el-button @click="fetchPreview" :loading="isLoading">获取预览</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<h3>Aime卡扫描</h3>
|
||||||
|
<el-form :inline="true" @submit.prevent="scanAimeQR">
|
||||||
|
<el-form-item label="二维码内容">
|
||||||
|
<el-input v-model="qrContent" placeholder="请输入Aime卡二维码内容" clearable style="width: 300px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="success" @click="scanAimeQR" :loading="isScanning">扫描并填充用户ID</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="card-container">
|
||||||
|
<el-card v-if="userData" header="完整用户信息" class="data-card">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="用户名">{{ userData.userName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Rating">{{ userData.playerRating }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最高Rating">{{ userData.highestRating }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="游玩次数">{{ userData.playCount }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="封禁状态">{{ userData.banState }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="previewData" header="用户预览" class="data-card">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="用户名">{{ previewData.userName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Rating">{{ previewData.playerRating }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="是否在线">{{ previewData.isLogin }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="封禁状态">{{ previewData.banState }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="inputUserId" header="玩家操作" class="data-card">
|
||||||
|
<el-collapse accordion>
|
||||||
|
<el-collapse-item title="通用操作" name="1">
|
||||||
|
<el-button type="success" @click="claimBonus" :loading="isLoading">领取登录奖励</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item title="道具管理" name="2">
|
||||||
|
<p>解锁道具 (例如: 种类 10 为搭档)。</p>
|
||||||
|
<el-form :inline="true" @submit.prevent="doUnlockItem">
|
||||||
|
<el-form-item><el-input v-model="itemKind" placeholder="道具种类" /></el-form-item>
|
||||||
|
<el-form-item><el-input v-model="itemIds" placeholder="道具ID (英文逗号分隔)" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="warning" @click="doUnlockItem" :loading="isLoading">解锁</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item title="票券管理" name="3">
|
||||||
|
<el-form :inline="true" @submit.prevent="buyTicket">
|
||||||
|
<el-form-item><el-input v-model="ticketType" placeholder="票券种类" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="buyTicket" :loading="isLoading">购买</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item title="分数管理" name="4">
|
||||||
|
<el-tabs type="border-card">
|
||||||
|
<el-tab-pane label="上传分数">
|
||||||
|
<el-form :inline="true" @submit.prevent="uploadScore">
|
||||||
|
<el-form-item><el-input v-model="scoreForm.musicId" placeholder="乐曲ID" /></el-form-item>
|
||||||
|
<el-form-item><el-input v-model="scoreForm.levelId" placeholder="难度 (0-4)" /></el-form-item>
|
||||||
|
<el-form-item><el-input v-model="scoreForm.achievement" placeholder="达成率 (例如: 1005000)" /></el-form-item>
|
||||||
|
<el-form-item><el-input v-model="scoreForm.dxScore" placeholder="DX分数" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="uploadScore" :loading="isLoading">上传</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="删除分数">
|
||||||
|
<el-form :inline="true" @submit.prevent="deleteScore">
|
||||||
|
<el-form-item><el-input v-model="scoreForm.musicId" placeholder="乐曲ID" /></el-form-item>
|
||||||
|
<el-form-item><el-input v-model="scoreForm.levelId" placeholder="难度 (0-4)" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="danger" @click="deleteScore" :loading="isLoading">删除</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-collapse-item>
|
||||||
|
<el-collapse-item title="高级/危险操作" name="5">
|
||||||
|
<el-form :inline="true" @submit.prevent="uploadToFish">
|
||||||
|
<el-form-item label="上传至水鱼"><el-input v-model="fishImportToken" type="password" placeholder="水鱼查分器Token" show-password /></el-form-item>
|
||||||
|
<el-form-item><el-button type="info" @click="uploadToFish" :loading="isLoading">上传</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-divider />
|
||||||
|
<el-form :inline="true" @submit.prevent="getAny">
|
||||||
|
<el-form-item label="通用接口调用"><el-input v-model="apiName" placeholder="API名称 (例如: GetUserChargeApi)" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="info" @click="getAny" :loading="isLoading">调用</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-card v-if="anyData" header="Result" style="margin-top: 10px;">
|
||||||
|
<pre>{{ JSON.stringify(anyData, null, 2) }}</pre>
|
||||||
|
</el-card>
|
||||||
|
<el-divider />
|
||||||
|
<el-button type="danger" @click="wipeTickets" :loading="isLoading">清空所有票券</el-button>
|
||||||
|
<el-button type="danger" @click="forceLogout" :loading="isLoading">强制登出</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card v-if="inputUserId" header="分数详情" style="width: 100%;">
|
||||||
|
<el-button @click="fetchScores" :loading="isLoading">获取所有分数</el-button>
|
||||||
|
<el-table v-if="musicResults" :data="musicResults" stripe style="width: 100%; margin-top: 20px;" height="400">
|
||||||
|
<el-table-column prop="musicId" label="乐曲ID" sortable />
|
||||||
|
<el-table-column prop="level" label="难度" sortable />
|
||||||
|
<el-table-column label="达成率" sortable>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ (scope.row.achievement / 10000).toFixed(4) }}%
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="playCount" label="游玩次数" sortable />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
</el-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.data-card {
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
frontend/tsconfig.app.json
Normal file
12
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/tsconfig.json
Normal file
11
frontend/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
frontend/tsconfig.node.json
Normal file
19
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
21
frontend/vite.config.ts
Normal file
21
frontend/vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
overlay: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user