mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-09 01:07:27 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21cf89b1b | ||
|
|
fbd5d83626 | ||
|
|
9fbdcd5edb | ||
|
|
88d949fc18 | ||
|
|
9636722709 | ||
|
|
9c90d6ef89 | ||
|
|
af03a48134 | ||
|
|
1fde2801ad | ||
|
|
f250701eca | ||
|
|
627985d1af | ||
|
|
7324c9bd76 | ||
|
|
426f65ea9e | ||
|
|
a7a9a4ba3d | ||
|
|
84b0e869a5 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,4 +17,7 @@ __pycache__/
|
||||
|
||||
# setting/config files
|
||||
latest version/config/
|
||||
latest version/config.py
|
||||
latest version/config.py
|
||||
|
||||
# song data
|
||||
latest version/database/songs/
|
||||
|
||||
68
README.md
68
README.md
@@ -1,7 +1,9 @@
|
||||
# Arcaea-server
|
||||
|
||||
一个微型的Arcaea本地服务器 A small local server for Arcaea
|
||||
|
||||
## 简介 Introduction
|
||||
|
||||
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||
|
||||
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
||||
@@ -14,7 +16,9 @@ This procedure is mainly used for study and research, and shall not be used for
|
||||
> It looks stupid, but it works!
|
||||
|
||||
## 特性 Features
|
||||
|
||||
有以下 We have:
|
||||
|
||||
- 登录、注册 Login and registration
|
||||
- 多设备登录 Multi device login
|
||||
- 成绩上传 Score upload
|
||||
@@ -24,7 +28,7 @@ This procedure is mainly used for study and research, and shall not be used for
|
||||
- 段位系统 Course system
|
||||
- Link Play
|
||||
- 好友系统 Friends
|
||||
- 数据同步 Data synchronization
|
||||
- 云端存档 Cloud save
|
||||
- 爬梯 Climbing steps
|
||||
- 自定义世界模式 Customizable World Mode
|
||||
- 自定义歌曲下载 Customizable songs download
|
||||
@@ -40,60 +44,49 @@ This procedure is mainly used for study and research, and shall not be used for
|
||||
- 服务器日志 Server log
|
||||
|
||||
没有以下 We don't have:
|
||||
|
||||
- 服务器安全性保证 Server security assurance
|
||||
|
||||
可能有问题 There may be problems:
|
||||
|
||||
- Recent 30
|
||||
- 一些歌曲的解锁 Some songs' unlocking
|
||||
- 同设备多共存登录 Multiple app logins on the same device
|
||||
|
||||
## 说明 Statement
|
||||
|
||||
只是很有趣,用处探索中。
|
||||
It is just so interesting. What it can do is under exploration.
|
||||
|
||||
|
||||
## 下载 Download
|
||||
|
||||
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
|
||||
|
||||
[Arcaea-CN official](https://arcaea.lowiro.com/zh)
|
||||
[Arcaea-lowi.ro](https://lowi.ro)
|
||||
[Arcaea-RhyDown](https://rhydown.com)
|
||||
[Arcaea-Konmai Academy](https://616.sb)
|
||||
|
||||
## 更新日志 Update log
|
||||
|
||||
只保留最新版本 Only keep the latest version.
|
||||
|
||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
||||
>
|
||||
> Tips: When updating, please keep the original database in case of data loss.
|
||||
> Tips: When updating, please keep the original database in case of data loss.
|
||||
|
||||
### Version 2.10.3
|
||||
|
||||
### Version 2.10.1
|
||||
- 适用于Arcaea 4.1.4版本 For Arcaea 4.1.4
|
||||
- 新搭档 **天音** 已解锁 Unlock the character **Amane**.
|
||||
- 为**天音**技能提供支持 Add support for the skill of **Amane**.
|
||||
- 现在配置文件可以是含有部分选项的文件或模块 At present the setting file can be a module or a file with some of options.
|
||||
- 添加`waitress`和`gevent`的部署方案支持,并支持日志记录 Add deployment mode `waitress` and `gevent`, and add support for the info log recording of them.
|
||||
- 为`songlist`添加解析器以指定可下载的文件 Add a parser for `songlist` to specify downloadable files.
|
||||
- 重构数据库初始化和数据迁移部分 Code refactoring for database initialization and migration.
|
||||
- 限制用户下载频率将使用第三方限制器,替代数据库 Add a custom limiter and use it for limiting users' download rate instead of using database.
|
||||
> 现在需要`limits`模块
|
||||
> Now `limits` module is required.
|
||||
- 为登录和API登录添加限制器 Add limiter for login and API login.
|
||||
- `sqlite3`数据库调整为WAL模式并增大缓存 Change journal mode to WAL and enlarge cache size for `sqlite3` database.
|
||||
- 将下载token放入内存中而不是文件数据库中 Put download token in memory database instead of filesystem database.
|
||||
- 加速`best_score`表多次查询,表现为歌曲排行榜查询性能提升 Accelerate multiple querying in `best_score` table, which results in performance improvement of song ranklist query.
|
||||
- 优化歌曲下载部分 Make some optimization for downloading songs.
|
||||
- **修复更新recent 10时可能出现的死循环问题 Fix a bug that there is a endless loop in calculating recent 10 updating.** (due to 6fcca179182775615115cdb255b3a8223831a8a0)
|
||||
- 修复课题模式成绩没有rating的问题 Fix a bug that scores in course mode cannot calculate rating.
|
||||
- 修正搭档数值 Fix a character's value.
|
||||
- 邮箱长度最大限制提升到64 Change the email max length to 64.
|
||||
- 新增API接口来获取用户身份与权限 Add a method of API for getting users' roles and powers.
|
||||
- 新增API接口来修改用户信息 Add a method of API to change the user's info.
|
||||
- 为API的`GET`请求添加`query`参数支持 Add support for the `query` param in API's `GET` requests.
|
||||
- 修复API的`best30`接口曲目无数据导致出错的问题 Fix a bug that `best30` of API cannot have scores whose songs are not in database.
|
||||
- 修复API的`recent30`接口用户成绩数量不足导致出错的问题 Fix a bug that users with no recent scores cannot get `recent30` via API.
|
||||
- 适用于Arcaea 4.2.0版本 For Arcaea 4.2.0
|
||||
- 新搭档 **拉格兰(Aria)** 已解锁 Unlock the character **Lagrange(Aria)**. (Lack of its values)
|
||||
- 新搭档 **忘却(Apophenia)** 已解锁 Unlock the character **Lethe(Apophenia)**.
|
||||
- 新增选项取消歌曲文件哈希预计算 Add an option to disable song file hash pre-calculation.
|
||||
- 新增对世界模式中地图本地限制歌曲解锁或挑战解锁以及地图中台阶上限制歌曲难度的支持 Add support for restricting songs' difficulty in the map's steps of world mode and locally restricting unlocking songs or challenges in the map of world mode.
|
||||
- 恢复使用云存档覆盖成绩的功能 Restore the feature that cloud save can be used to cover best scores.
|
||||
- 捕获`Authorization`不在请求头导致的报错 Capture error that the request does not have `Authorization` in header.
|
||||
- 修复客户端版本校验中请求头不存在`AppVersion`也能通过校验的逻辑错误 Fix a bug that headers without `AppVersion` are allowed in client version checking.
|
||||
- 新增增删改歌曲信息的API接口 Add some API endpoints, including creating, changing, deleting song info.
|
||||
|
||||
## 运行环境与依赖 Running environment and requirements
|
||||
|
||||
- Windows/Linux/Mac OS/Android...
|
||||
- Python >= 3.6
|
||||
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
|
||||
@@ -106,34 +99,41 @@ It is just so interesting. What it can do is under exploration.
|
||||
-->
|
||||
|
||||
## 使用说明 Instruction for use
|
||||
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use)
|
||||
|
||||
## 注意 Attentions
|
||||
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
|
||||
|
||||
|
||||
## Q&A
|
||||
|
||||
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
|
||||
|
||||
|
||||
## 鸣谢 Thanks
|
||||
~~歌曲数据库来自 Using song database from
|
||||
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||
|
||||
歌曲数据库来自 Using song database from
|
||||
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
|
||||
|
||||
> 从v2.9开始不再提供歌曲数据
|
||||
> Since v2.9, song data will not be provided.
|
||||
|
||||
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
|
||||
|
||||
## 联系方式 Contact
|
||||
|
||||
如有必要,可以联系本人 Contact me if necessary
|
||||
邮箱 Email:th84292@foxmail.com
|
||||
|
||||
## 支持一下 Support me
|
||||
|
||||
生活不易。 Life is not easy.
|
||||
[支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg)
|
||||
[微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png)
|
||||
|
||||
## 使用许可 Use license
|
||||
|
||||
[MIT](LICENSE) © Lost
|
||||
|
||||
@@ -48,13 +48,14 @@ def role_required(request, powers=[]):
|
||||
return decorator
|
||||
|
||||
|
||||
def request_json_handle(request, required_keys=[], optional_keys=[]):
|
||||
def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False):
|
||||
'''
|
||||
提取post参数,返回dict,写成了修饰器\
|
||||
parameters: \
|
||||
`request`: `Request` - 当前请求\
|
||||
`required_keys`: `list` - 必须的参数\
|
||||
`optional_keys`: `list` - 可选的参数
|
||||
`optional_keys`: `list` - 可选的参数\
|
||||
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
|
||||
'''
|
||||
|
||||
def decorator(view):
|
||||
@@ -67,8 +68,11 @@ def request_json_handle(request, required_keys=[], optional_keys=[]):
|
||||
else:
|
||||
if request.method == 'GET' and 'query' in request.args:
|
||||
# 处理axios没法GET传data的问题
|
||||
json_data = loads(
|
||||
b64decode(request.args['query']).decode())
|
||||
try:
|
||||
json_data = loads(
|
||||
b64decode(request.args['query']).decode())
|
||||
except:
|
||||
raise PostError(api_error_code=-105)
|
||||
else:
|
||||
json_data = {}
|
||||
|
||||
@@ -81,6 +85,9 @@ def request_json_handle(request, required_keys=[], optional_keys=[]):
|
||||
if key in json_data:
|
||||
data[key] = json_data[key]
|
||||
|
||||
if must_change and not data:
|
||||
return error_return(PostError('No change', api_error_code=-100))
|
||||
|
||||
return view(data, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from core.error import ArcError
|
||||
from flask import jsonify
|
||||
|
||||
from core.error import ArcError
|
||||
|
||||
default_error = ArcError('Unknown Error')
|
||||
|
||||
|
||||
@@ -9,6 +10,7 @@ CODE_MSG = {
|
||||
-1: 'See status code', # 基础错误
|
||||
-2: 'No data',
|
||||
-3: 'No data or user', # 不确定是无数据还是无用户
|
||||
-4: 'Data exist',
|
||||
-100: 'Invalid post data', # 1xx数据错误
|
||||
-101: 'Invalid data type',
|
||||
-102: 'Invalid query parameter',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from core.error import NoData
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.error import DataExist, InputError, NoData
|
||||
from core.rank import RankList
|
||||
from core.song import Song
|
||||
from core.sql import Connect, Query, Sql
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import success_return
|
||||
@@ -20,6 +22,39 @@ def songs_song_get(user, song_id):
|
||||
return success_return(s.to_dict())
|
||||
|
||||
|
||||
@bp.route('/<string:song_id>', methods=['PUT'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, optional_keys=['name', 'charts'], must_change=True)
|
||||
@api_try
|
||||
def songs_song_put(data, user, song_id):
|
||||
'''修改歌曲信息'''
|
||||
with Connect() as c:
|
||||
s = Song(c, song_id).select()
|
||||
if 'name' in data:
|
||||
s.name = str(data['name'])
|
||||
if 'charts' in data:
|
||||
for i in data['charts']:
|
||||
if 'difficulty' in i and 'chart_const' in i:
|
||||
s.charts[i['difficulty']].defnum = round(
|
||||
i['chart_const'] * 10)
|
||||
|
||||
s.update()
|
||||
return success_return(s.to_dict())
|
||||
|
||||
|
||||
@bp.route('/<string:song_id>', methods=['DELETE'])
|
||||
@role_required(request, ['change'])
|
||||
@api_try
|
||||
def songs_song_delete(user, song_id):
|
||||
'''删除歌曲信息'''
|
||||
with Connect() as c:
|
||||
s = Song(c, song_id)
|
||||
if not s.select_exists():
|
||||
raise NoData(f'No such song: `{song_id}`')
|
||||
s.delete()
|
||||
return success_return()
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_song_info'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@@ -40,3 +75,41 @@ def songs_get(data, user):
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
return success_return([x.to_dict() for x in r])
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, ['song_id', 'charts'], ['name'])
|
||||
@api_try
|
||||
def songs_post(data, user):
|
||||
'''添加歌曲信息'''
|
||||
with Connect() as c:
|
||||
s = Song(c).from_dict(data)
|
||||
if s.select_exists():
|
||||
raise DataExist(f'Song `{s.song_id}` already exists')
|
||||
s.insert()
|
||||
return success_return(s.to_dict())
|
||||
|
||||
|
||||
@bp.route('/<string:song_id>/<int:difficulty>/rank', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_song_rank', 'select_song_rank_top'])
|
||||
@request_json_handle(request, optional_keys=['limit'])
|
||||
@api_try
|
||||
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
|
||||
'''查询歌曲某个难度的成绩排行榜,和游戏内接口相似,只允许limit'''
|
||||
if difficulty not in [0, 1, 2, 3]:
|
||||
raise InputError('Difficulty must be 0, 1, 2 or 3')
|
||||
limit = data.get('limit', 20)
|
||||
if not isinstance(limit, int):
|
||||
raise InputError('Limit must be int')
|
||||
if user.role.only_has_powers(['select_song_rank_top'], ['select', 'select_song_rank']):
|
||||
# 限制低权限只能查询前20名
|
||||
if limit > 20 or limit < 0:
|
||||
limit = 20
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(song_id, difficulty)
|
||||
rank_list.limit = limit
|
||||
# 不检查歌曲是否存在,不存在返回的是空列表
|
||||
rank_list.select_top()
|
||||
return success_return(rank_list.to_dict_list())
|
||||
|
||||
@@ -78,7 +78,7 @@ def users_user_get(user, user_id):
|
||||
|
||||
@bp.route('/<int:user_id>', methods=['PUT'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'])
|
||||
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'], must_change=True)
|
||||
@api_try
|
||||
def users_user_put(data, user, user_id):
|
||||
'''修改一个用户'''
|
||||
@@ -103,8 +103,7 @@ def users_user_put(data, user, user_id):
|
||||
raise InputError('Ticket must be int')
|
||||
u.ticket = data['ticket']
|
||||
r['ticket'] = u.ticket
|
||||
if r:
|
||||
u.update_columns(d=r)
|
||||
u.update_columns(d=r)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,19 @@ class Role:
|
||||
'''判断role是否有power'''
|
||||
return any(power_id == i.power_id for i in self.powers)
|
||||
|
||||
def only_has_powers(self, power_ids: list, anti_power_ids: list = None) -> bool:
|
||||
'''判断role是否全有power_ids里的power,且没有anti_power_ids里的任何一个power'''
|
||||
flags = [False] * len(power_ids)
|
||||
if anti_power_ids is None:
|
||||
anti_power_ids = []
|
||||
for i in self.powers:
|
||||
if i.power_id in anti_power_ids:
|
||||
return False
|
||||
for j, k in enumerate(power_ids):
|
||||
if i.power_id == k:
|
||||
flags[j] = True
|
||||
return all(flags)
|
||||
|
||||
def select_from_id(self, role_id: int = None) -> 'Role':
|
||||
'''用role_id查询role'''
|
||||
if role_id is not None:
|
||||
@@ -121,11 +134,12 @@ class APIUser(UserOnline):
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The user `%s` does not exist.' %
|
||||
self.name, api_error_code=-201)
|
||||
self.name, api_error_code=-201, status=401)
|
||||
if x[1] == '':
|
||||
raise UserBan('The user `%s` is banned.' % self.name)
|
||||
if self.hash_pwd != x[1]:
|
||||
raise NoAccess('The password is incorrect.', api_error_code=-201)
|
||||
raise NoAccess('The password is incorrect.',
|
||||
api_error_code=-201, status=401)
|
||||
|
||||
self.user_id = x[0]
|
||||
now = int(time() * 1000)
|
||||
|
||||
@@ -10,6 +10,8 @@ class Config:
|
||||
USE_PROXY_FIX = False
|
||||
USE_CORS = False
|
||||
|
||||
SONG_FILE_HASH_PRE_CALCULATE = True
|
||||
|
||||
GAME_API_PREFIX = '/join/21'
|
||||
|
||||
ALLOW_APPVERSION = [] # list[str]
|
||||
@@ -50,6 +52,8 @@ class Config:
|
||||
ALLOW_LOGIN_SAME_DEVICE = False
|
||||
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
||||
|
||||
ALLOW_SCORE_WITH_NO_SONG = True
|
||||
|
||||
ALLOW_INFO_LOG = False
|
||||
ALLOW_WARNING_LOG = False
|
||||
|
||||
@@ -77,6 +81,7 @@ class Config:
|
||||
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
|
||||
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
|
||||
DATABASE_INIT_PATH = './database/init/'
|
||||
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
|
||||
|
||||
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
||||
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .config_manager import Config
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.10.1'
|
||||
ARCAEA_SERVER_VERSION = 'v2.10.3'
|
||||
|
||||
|
||||
class Constant:
|
||||
@@ -35,6 +35,7 @@ class Constant:
|
||||
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
|
||||
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
|
||||
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
||||
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
||||
|
||||
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
||||
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
||||
|
||||
@@ -5,6 +5,7 @@ from time import time
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import NoAccess
|
||||
from .limiter import ArcLimiter
|
||||
@@ -20,15 +21,6 @@ def get_song_file_md5(song_id: str, file_name: str) -> str:
|
||||
return get_file_md5(path)
|
||||
|
||||
|
||||
def initialize_songfile():
|
||||
'''初始化歌曲数据的md5信息'''
|
||||
get_song_file_md5.cache_clear()
|
||||
x = DownloadList()
|
||||
x.url_flag = False
|
||||
x.add_songs()
|
||||
del x
|
||||
|
||||
|
||||
class SonglistParser:
|
||||
'''songlist文件解析器'''
|
||||
|
||||
@@ -51,7 +43,7 @@ class SonglistParser:
|
||||
self.parse()
|
||||
|
||||
@staticmethod
|
||||
def is_available_file(song_id: str, file_name: str) -> list:
|
||||
def is_available_file(song_id: str, file_name: str) -> bool:
|
||||
'''判断文件是否允许被下载'''
|
||||
if song_id not in SonglistParser.songs:
|
||||
# songlist没有,则只限制文件名
|
||||
@@ -176,7 +168,7 @@ class UserDownload:
|
||||
|
||||
class DownloadList(UserDownload):
|
||||
'''
|
||||
下载列表类\
|
||||
下载列表类
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
@@ -189,13 +181,23 @@ class DownloadList(UserDownload):
|
||||
self.downloads: list = []
|
||||
self.urls: dict = {}
|
||||
|
||||
@classmethod
|
||||
def initialize_cache(cls) -> None:
|
||||
'''初始化歌曲数据缓存,包括md5、文件目录遍历、解析songlist'''
|
||||
SonglistParser()
|
||||
if Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||
x = cls()
|
||||
x.url_flag = False
|
||||
x.add_songs()
|
||||
del x
|
||||
|
||||
@staticmethod
|
||||
def clear_all_cache():
|
||||
def clear_all_cache() -> None:
|
||||
'''清除所有歌曲文件有关缓存'''
|
||||
get_song_file_md5.cache_clear()
|
||||
DownloadList.get_one_song_file_names.cache_clear()
|
||||
DownloadList.get_all_song_ids.cache_clear()
|
||||
SonglistParser()
|
||||
SonglistParser.songs = {}
|
||||
|
||||
def clear_download_token(self) -> None:
|
||||
'''清除过期下载链接'''
|
||||
@@ -212,9 +214,10 @@ class DownloadList(UserDownload):
|
||||
def get_one_song_file_names(song_id: str) -> list:
|
||||
'''获取一个歌曲文件夹下的所有合法文件名,有lru缓存'''
|
||||
r = []
|
||||
for i in os.listdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)):
|
||||
if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and SonglistParser.is_available_file(song_id, i):
|
||||
r.append(i)
|
||||
for i in os.scandir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)):
|
||||
file_name = i.name
|
||||
if i.is_file() and SonglistParser.is_available_file(song_id, file_name):
|
||||
r.append(file_name)
|
||||
return r
|
||||
|
||||
def add_one_song(self, song_id: str) -> None:
|
||||
@@ -265,7 +268,7 @@ class DownloadList(UserDownload):
|
||||
@lru_cache()
|
||||
def get_all_song_ids() -> list:
|
||||
'''获取全歌曲文件夹列表,有lru缓存'''
|
||||
return list(filter(lambda x: os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, x)), os.listdir(Constant.SONG_FILE_FOLDER_PATH)))
|
||||
return [i.name for i in os.scandir(Constant.SONG_FILE_FOLDER_PATH) if i.is_dir()]
|
||||
|
||||
def add_songs(self, song_ids: list = None) -> None:
|
||||
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
|
||||
|
||||
@@ -19,7 +19,9 @@ class InputError(ArcError):
|
||||
|
||||
class DataExist(ArcError):
|
||||
'''数据存在'''
|
||||
pass
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-4, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class NoData(ArcError):
|
||||
|
||||
@@ -4,11 +4,12 @@ from importlib import import_module
|
||||
from json import load
|
||||
from shutil import copy, copy2
|
||||
from time import time
|
||||
from traceback import format_exc
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.constant import ARCAEA_SERVER_VERSION
|
||||
from core.course import Course
|
||||
from core.download import SonglistParser
|
||||
from core.download import DownloadList
|
||||
from core.purchase import Purchase
|
||||
from core.sql import Connect, DatabaseMigrator, MemoryDatabase
|
||||
from core.user import UserRegister
|
||||
@@ -90,10 +91,10 @@ class DatabaseInit:
|
||||
self.c.execute('''insert into item values(?,?,?)''',
|
||||
('anni5tix', 'anni5tix', 1))
|
||||
|
||||
with open(self.pack_path, 'r') as f:
|
||||
with open(self.pack_path, 'rb') as f:
|
||||
self.insert_purchase_item(load(f))
|
||||
|
||||
with open(self.single_path, 'r') as f:
|
||||
with open(self.single_path, 'rb') as f:
|
||||
self.insert_purchase_item(load(f))
|
||||
|
||||
self.c.execute(
|
||||
@@ -105,7 +106,7 @@ class DatabaseInit:
|
||||
def course_init(self) -> None:
|
||||
'''初始化课题信息'''
|
||||
courses = []
|
||||
with open(self.course_path, 'r', encoding='utf-8') as f:
|
||||
with open(self.course_path, 'rb') as f:
|
||||
courses = load(f)
|
||||
for i in courses:
|
||||
x = Course(self.c).from_dict(i)
|
||||
@@ -157,35 +158,73 @@ class DatabaseInit:
|
||||
self.admin_init()
|
||||
|
||||
|
||||
class FileChecker:
|
||||
class LogDatabaseInit:
|
||||
def __init__(self, db_path: str = Config.SQLITE_LOG_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
|
||||
self.db_path = db_path
|
||||
self.init_folder_path = init_folder_path
|
||||
self.c = None
|
||||
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
@property
|
||||
def sql_path(self) -> str:
|
||||
return os.path.join(self.init_folder_path, 'log_tables.sql')
|
||||
|
||||
def table_init(self) -> None:
|
||||
'''初始化数据库结构'''
|
||||
with open(self.sql_path, 'r') as f:
|
||||
self.c.executescript(f.read())
|
||||
self.c.execute(
|
||||
'''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION})
|
||||
|
||||
def init(self) -> None:
|
||||
with Connect(self.db_path) as c:
|
||||
self.c = c
|
||||
self.table_init()
|
||||
|
||||
|
||||
class FileChecker:
|
||||
'''文件检查及初始化类'''
|
||||
|
||||
def __init__(self, logger=None):
|
||||
self.logger = logger
|
||||
|
||||
def check_file(self, file_path: str) -> bool:
|
||||
f = os.path.isfile(file_path)
|
||||
if not f:
|
||||
self.app.logger.warning('File `%s` is missing.' % file_path)
|
||||
self.logger.warning('File `%s` is missing.' % file_path)
|
||||
return f
|
||||
|
||||
def check_folder(self, folder_path: str) -> bool:
|
||||
f = os.path.isdir(folder_path)
|
||||
if not f:
|
||||
self.app.logger.warning('Folder `%s` is missing.' % folder_path)
|
||||
self.logger.warning('Folder `%s` is missing.' % folder_path)
|
||||
return f
|
||||
|
||||
def check_update_database(self) -> bool:
|
||||
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
||||
# 新建日志数据库
|
||||
try:
|
||||
self.logger.info(
|
||||
f'Try to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||
LogDatabaseInit().init()
|
||||
self.logger.info(
|
||||
f'Success to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.error(
|
||||
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||
return False
|
||||
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
||||
# 新建数据库
|
||||
try:
|
||||
self.app.logger.info(
|
||||
self.logger.info(
|
||||
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
DatabaseInit().init()
|
||||
self.app.logger.info(
|
||||
self.logger.info(
|
||||
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
except:
|
||||
self.app.logger.warning(
|
||||
'Fail to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning(
|
||||
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
return False
|
||||
else:
|
||||
# 检查更新
|
||||
@@ -197,10 +236,10 @@ class FileChecker:
|
||||
x = None
|
||||
# 数据库自动更新,不强求
|
||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
||||
self.app.logger.warning(
|
||||
self.logger.warning(
|
||||
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
|
||||
try:
|
||||
self.app.logger.info(
|
||||
self.logger.info(
|
||||
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
|
||||
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
||||
@@ -224,11 +263,12 @@ class FileChecker:
|
||||
DatabaseInit().init()
|
||||
self.update_database(temp_path)
|
||||
|
||||
self.app.logger.info(
|
||||
self.logger.info(
|
||||
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
|
||||
except ValueError:
|
||||
self.app.logger.warning(
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning(
|
||||
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
|
||||
return True
|
||||
@@ -240,9 +280,22 @@ class FileChecker:
|
||||
DatabaseMigrator(old_path, new_path).update_database()
|
||||
os.remove(old_path)
|
||||
|
||||
def check_song_file(self) -> bool:
|
||||
'''检查song有关文件并初始化缓存'''
|
||||
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
||||
self.logger.info("Start to initialize song data...")
|
||||
try:
|
||||
DownloadList.initialize_cache()
|
||||
if not Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||
self.logger.info('Song file hash pre-calculate is disabled.')
|
||||
self.logger.info('Complete!')
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning('Initialization error!')
|
||||
f = False
|
||||
return f
|
||||
|
||||
def check_before_run(self) -> bool:
|
||||
'''运行前检查,返回布尔值'''
|
||||
# TODO: try
|
||||
MemoryDatabase() # 初始化内存数据库
|
||||
SonglistParser() # 解析songlist
|
||||
return self.check_folder(Config.SONG_FILE_FOLDER_PATH) & self.check_update_database()
|
||||
return self.check_song_file() & self.check_update_database()
|
||||
|
||||
244
latest version/core/operation.py
Normal file
244
latest version/core/operation.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from .download import DownloadList
|
||||
from .save import SaveData
|
||||
from .score import Score
|
||||
from .sql import Connect, Sql
|
||||
from .user import User
|
||||
|
||||
|
||||
class BaseOperation:
|
||||
_name: str = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __call__(self, *args, **kwargs) -> None:
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
def set_params(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def run(self, *args, **kwargs) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RefreshAllScoreRating(BaseOperation):
|
||||
'''
|
||||
刷新所有成绩的评分
|
||||
'''
|
||||
_name = 'refresh_all_score_rating'
|
||||
|
||||
def run(self):
|
||||
# 追求效率,不用Song类,尽量不用对象
|
||||
# 但其实还是很慢
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||
x = c.fetchall()
|
||||
|
||||
songs = [i[0] for i in x]
|
||||
c.execute(
|
||||
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
||||
|
||||
for i in x:
|
||||
for j in range(0, 4):
|
||||
defnum = -10 # 没在库里的全部当做定数-10
|
||||
if i[j+1] is not None and i[j+1] > 0:
|
||||
defnum = float(i[j+1]) / 10
|
||||
|
||||
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
|
||||
'a': i[0], 'b': j})
|
||||
y = c.fetchall()
|
||||
values = []
|
||||
where_values = []
|
||||
for k in y:
|
||||
ptt = Score.calculate_rating(defnum, k[1])
|
||||
if ptt < 0:
|
||||
ptt = 0
|
||||
values.append((ptt,))
|
||||
where_values.append((k[0], i[0], j))
|
||||
if values:
|
||||
Sql(c).update_many('best_score', ['rating'], values, [
|
||||
'user_id', 'song_id', 'difficulty'], where_values)
|
||||
|
||||
|
||||
class RefreshSongFileCache(BaseOperation):
|
||||
'''
|
||||
刷新歌曲文件缓存,包括文件hash缓存重建、文件目录重遍历、songlist重解析
|
||||
注意在设置里预先计算关闭的情况下,文件hash不会计算
|
||||
'''
|
||||
_name = 'refresh_song_file_cache'
|
||||
|
||||
def run(self):
|
||||
DownloadList.clear_all_cache()
|
||||
DownloadList.initialize_cache()
|
||||
|
||||
|
||||
class SaveUpdateScore(BaseOperation):
|
||||
'''
|
||||
云存档更新成绩,是覆盖式更新\
|
||||
提供user参数时,只更新该用户的成绩,否则更新所有用户的成绩
|
||||
'''
|
||||
_name = 'save_update_score'
|
||||
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||
if user_id is not None:
|
||||
self.user = User()
|
||||
self.user.user_id = int(user_id)
|
||||
|
||||
def run(self, user=None):
|
||||
'''
|
||||
parameter:
|
||||
`user` - `User`类或子类的实例
|
||||
'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
if self.user is not None and self.user.user_id is not None:
|
||||
self._one_user_update()
|
||||
else:
|
||||
self._all_update()
|
||||
|
||||
def _one_user_update(self):
|
||||
with Connect() as c:
|
||||
save = SaveData(c)
|
||||
save.select_scores(self.user)
|
||||
|
||||
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
|
||||
for i in save.clearlamps_data}
|
||||
|
||||
song_id_1 = [i['song_id'] for i in save.scores_data]
|
||||
song_id_2 = [i['song_id'] for i in save.clearlamps_data]
|
||||
song_id = list(set(song_id_1 + song_id_2))
|
||||
|
||||
c.execute(
|
||||
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
|
||||
x = c.fetchall()
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||
for i in x} # chart const * 10
|
||||
|
||||
new_scores = []
|
||||
for i in save.scores_data:
|
||||
rating = 0
|
||||
if i['song_id'] in song_chart_const:
|
||||
rating = Score.calculate_rating(
|
||||
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
||||
if rating < 0:
|
||||
rating = 0
|
||||
|
||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||
if y in clear_state:
|
||||
clear_type = clear_state[y]
|
||||
else:
|
||||
clear_type = 0
|
||||
|
||||
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
||||
|
||||
c.executemany(
|
||||
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||
|
||||
def _all_update(self):
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||
for i in c.fetchall()} # chart const * 10
|
||||
c.execute('''select user_id from user_save''')
|
||||
for y in c.fetchall():
|
||||
user = User()
|
||||
user.user_id = y[0]
|
||||
save = SaveData(c)
|
||||
save.select_scores(user)
|
||||
|
||||
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
|
||||
for i in save.clearlamps_data}
|
||||
|
||||
new_scores = []
|
||||
for i in save.scores_data:
|
||||
rating = 0
|
||||
if i['song_id'] in song_chart_const:
|
||||
rating = Score.calculate_rating(
|
||||
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
||||
if rating < 0:
|
||||
rating = 0
|
||||
|
||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||
if y in clear_state:
|
||||
clear_type = clear_state[y]
|
||||
else:
|
||||
clear_type = 0
|
||||
|
||||
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
||||
|
||||
c.executemany(
|
||||
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||
|
||||
|
||||
class UnlockUserItem(BaseOperation):
|
||||
'''
|
||||
全解锁/锁定用户物品\
|
||||
提供user参数时,只更新该用户的,否则更新所有用户的
|
||||
'''
|
||||
_name = 'unlock_user_item'
|
||||
|
||||
ALLOW_TYPES = ['single', 'pack', 'world_song',
|
||||
'course_banner', 'world_unlock']
|
||||
|
||||
def __init__(self, user=None, method: str = 'unlock', item_types: list = ['single', 'pack']):
|
||||
self.user = user
|
||||
self.set_params(method=method, item_types=item_types)
|
||||
|
||||
def set_params(self, user_id: int = None, method: str = 'unlock', item_types: list = ['single', 'pack'], *args, **kwargs):
|
||||
if user_id is not None:
|
||||
self.user = User()
|
||||
self.user.user_id = int(user_id)
|
||||
if method in ['unlock', 'lock']:
|
||||
self.method = method
|
||||
if isinstance(item_types, list) and all([i in self.ALLOW_TYPES for i in item_types]):
|
||||
self.item_types = item_types
|
||||
|
||||
def run(self):
|
||||
if self.user is not None and self.user.user_id is not None:
|
||||
if self.method == 'unlock':
|
||||
self._one_user_insert()
|
||||
else:
|
||||
self._one_user_delete()
|
||||
else:
|
||||
if self.method == 'unlock':
|
||||
self._all_insert()
|
||||
else:
|
||||
self._all_delete()
|
||||
|
||||
def _one_user_insert(self):
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||
sql_list = [(self.user.user_id, i[0], i[1])
|
||||
for i in c.fetchall()]
|
||||
c.executemany(
|
||||
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
|
||||
|
||||
def _all_insert(self):
|
||||
with Connect() as c:
|
||||
c.execute('''select user_id from user''')
|
||||
x = c.fetchall()
|
||||
c.execute(
|
||||
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||
y = c.fetchall()
|
||||
sql_list = [(i[0], j[0], j[1])
|
||||
for i in x for j in y]
|
||||
c.executemany(
|
||||
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
|
||||
|
||||
def _one_user_delete(self):
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
f'''delete from user_item where user_id = ? and type in ({','.join(['?'] * len(self.item_types))})''', (self.user.user_id, *self.item_types))
|
||||
|
||||
def _all_delete(self):
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||
@@ -22,6 +22,8 @@ class Purchase:
|
||||
|
||||
self.items: list = []
|
||||
|
||||
# TODO: "discount_reason": "extend"
|
||||
|
||||
@property
|
||||
def price_displayed(self) -> int:
|
||||
'''
|
||||
|
||||
@@ -3,7 +3,7 @@ from time import time
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import InputError
|
||||
from .error import InputError, NoData
|
||||
from .util import md5
|
||||
|
||||
|
||||
@@ -54,6 +54,20 @@ class SaveData:
|
||||
}
|
||||
}
|
||||
|
||||
def select_scores(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.user = user
|
||||
self.c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''',
|
||||
{'a': user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData(f'User `{user.user_id}` has no cloud save data')
|
||||
|
||||
self.scores_data: list = json.loads(x[0])[""]
|
||||
self.clearlamps_data: list = json.loads(x[1])[""]
|
||||
|
||||
def select_all(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
|
||||
@@ -7,7 +7,7 @@ from .course import CoursePlay
|
||||
from .error import NoData, StaminaNotEnough
|
||||
from .item import ItemCore
|
||||
from .song import Chart
|
||||
from .sql import Query, Sql
|
||||
from .sql import Connect, Query, Sql
|
||||
from .util import md5
|
||||
from .world import WorldPlay
|
||||
|
||||
@@ -102,8 +102,8 @@ class Score:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def calculate_rating(defnum: int, score: int) -> float:
|
||||
'''计算rating,谱面定数小于等于0视为Unrank,这里的defnum = Chart const'''
|
||||
def calculate_rating(defnum: float, score: int) -> float:
|
||||
'''计算rating,谱面定数小于等于0视为Unrank,返回值会为-1,这里的defnum = Chart const'''
|
||||
if not defnum or defnum <= 0:
|
||||
# 谱面没定数或者定数小于等于0被视作Unrank
|
||||
return -1
|
||||
@@ -408,6 +408,12 @@ class UserPlay(UserScore):
|
||||
|
||||
self.ptt.insert_recent_30()
|
||||
|
||||
def record_score(self) -> None:
|
||||
'''向log数据库记录分数,请注意列名不同'''
|
||||
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2:
|
||||
c2.execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
|
||||
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
|
||||
|
||||
def upload_score(self) -> None:
|
||||
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
||||
self.get_play_state()
|
||||
@@ -420,6 +426,9 @@ class UserPlay(UserScore):
|
||||
|
||||
self.time_played = int(time())
|
||||
|
||||
# 记录分数
|
||||
self.record_score()
|
||||
|
||||
# recent更新
|
||||
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .error import NoData
|
||||
from .config_manager import Config
|
||||
|
||||
|
||||
class Chart:
|
||||
@@ -33,14 +34,16 @@ class Chart:
|
||||
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
self.defnum = -10
|
||||
# raise NoData('The song `%s` does not exist.' % self.song_id)
|
||||
if Config.ALLOW_SCORE_WITH_NO_SONG:
|
||||
self.defnum = -10
|
||||
else:
|
||||
raise NoData(f'The song `{self.song_id}` does not exist.', 120)
|
||||
else:
|
||||
self.defnum = x[self.difficulty]
|
||||
|
||||
|
||||
class Song:
|
||||
def __init__(self, c=None, song_id=None) -> None:
|
||||
def __init__(self, c=None, song_id: str = None) -> None:
|
||||
self.c = c
|
||||
self.song_id: str = song_id
|
||||
self.name: str = None
|
||||
@@ -65,10 +68,39 @@ class Song:
|
||||
self.charts[3].defnum = x[5]
|
||||
return self
|
||||
|
||||
def from_dict(self, d: dict) -> 'Song':
|
||||
self.song_id = d['song_id']
|
||||
self.name = d.get('name', '')
|
||||
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
|
||||
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)]
|
||||
for i in range(4):
|
||||
self.charts[i].defnum = -10
|
||||
for chart in d['charts']:
|
||||
self.charts[chart['difficulty']].defnum = round(
|
||||
chart['chart_const'] * 10)
|
||||
return self
|
||||
|
||||
def delete(self) -> None:
|
||||
self.c.execute(
|
||||
'''delete from chart where song_id=?''', (self.song_id,))
|
||||
|
||||
def update(self) -> None:
|
||||
'''全部更新'''
|
||||
self.c.execute(
|
||||
'''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.song_id))
|
||||
|
||||
def insert(self) -> None:
|
||||
self.c.execute(
|
||||
'''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum))
|
||||
|
||||
def select_exists(self, song_id: str = None) -> bool:
|
||||
if song_id is not None:
|
||||
self.song_id = song_id
|
||||
|
||||
self.c.execute(
|
||||
'''select exists(select * from chart where song_id=?)''', (self.song_id,))
|
||||
return bool(self.c.fetchone()[0])
|
||||
|
||||
def select(self, song_id: str = None) -> 'Song':
|
||||
if song_id is not None:
|
||||
self.song_id = song_id
|
||||
@@ -77,6 +109,6 @@ class Song:
|
||||
'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The song `%s` does not exist.' % self.song_id)
|
||||
raise NoData(f'The song `{self.song_id}` does not exist.')
|
||||
|
||||
return self.from_list(x)
|
||||
|
||||
@@ -2,16 +2,15 @@ import sqlite3
|
||||
import traceback
|
||||
from atexit import register
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError
|
||||
|
||||
|
||||
class Connect:
|
||||
# 数据库连接类,上下文管理
|
||||
logger = None
|
||||
|
||||
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False):
|
||||
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db\
|
||||
接受:文件路径\
|
||||
@@ -19,6 +18,8 @@ class Connect:
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.in_memory: bool = in_memory
|
||||
if logger is not None:
|
||||
self.logger = logger
|
||||
|
||||
def __enter__(self) -> sqlite3.Cursor:
|
||||
if self.in_memory:
|
||||
@@ -37,7 +38,7 @@ class Connect:
|
||||
else:
|
||||
self.conn.rollback()
|
||||
|
||||
current_app.logger.error(
|
||||
self.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
|
||||
self.conn.commit()
|
||||
@@ -49,9 +50,9 @@ class Connect:
|
||||
class Query:
|
||||
'''查询参数类'''
|
||||
|
||||
def __init__(self, query_able: list = None, quzzy_query_able: list = None, sort_able: list = None) -> None:
|
||||
def __init__(self, query_able: list = None, fuzzy_query_able: list = None, sort_able: list = None) -> None:
|
||||
self.query_able: list = query_able # None表示不限制
|
||||
self.quzzy_query_able: list = quzzy_query_able # None表示不限制
|
||||
self.fuzzy_query_able: list = fuzzy_query_able # None表示不限制
|
||||
self.sort_able: list = sort_able
|
||||
|
||||
self.__limit: int = -1
|
||||
@@ -115,7 +116,7 @@ class Query:
|
||||
def fuzzy_query_append(self, fuzzy_query: dict) -> None:
|
||||
if not isinstance(fuzzy_query, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.quzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.quzzy_query_able)):
|
||||
if self.fuzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.fuzzy_query_able)):
|
||||
raise InputError(api_error_code=-102)
|
||||
if not self.__fuzzy_query:
|
||||
self.__fuzzy_query = fuzzy_query
|
||||
@@ -216,7 +217,7 @@ class Sql:
|
||||
return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')'
|
||||
|
||||
@staticmethod
|
||||
def get_update_sql(table_name: str, d: dict = {}, query: 'Query' = None) -> str:
|
||||
def get_update_sql(table_name: str, d: dict = None, query: 'Query' = None):
|
||||
if not d:
|
||||
return None
|
||||
sql_list = []
|
||||
@@ -245,6 +246,13 @@ class Sql:
|
||||
|
||||
return sql, sql_list
|
||||
|
||||
@staticmethod
|
||||
def get_update_many_sql(table_name: str, key: list = None, where_key: list = None) -> str:
|
||||
'''拼接update语句,这里不用Query类,也不用字典,请注意只返回sql语句'''
|
||||
if not key or not where_key:
|
||||
return None
|
||||
return f"update {table_name} set {','.join([f'{k}=?' for k in key])} where {' and '.join([f'{k}=?' for k in where_key])}"
|
||||
|
||||
@staticmethod
|
||||
def get_delete_sql(table_name: str, query: 'Query' = None):
|
||||
'''拼接删除语句,query中只有query和fuzzy_query会被处理'''
|
||||
@@ -304,6 +312,13 @@ class Sql:
|
||||
sql, sql_list = self.get_update_sql(table_name, d, query)
|
||||
self.c.execute(sql, sql_list)
|
||||
|
||||
def update_many(self, table_name: str, key: list, value_list: list, where_key: list, where_value_list: list) -> None:
|
||||
'''单表内行update多句sql语句,这里不用Query类,也不用字典,要求值list长度一致,有点像insert_many'''
|
||||
if not key or not value_list or not where_key or not where_value_list or not len(key) == len(value_list[0]) or not len(where_key) == len(where_value_list[0]) or not len(value_list) == len(where_value_list):
|
||||
raise ValueError
|
||||
self.c.executemany(self.get_update_many_sql(
|
||||
table_name, key, where_key), [x + y for x, y in zip(value_list, where_value_list)])
|
||||
|
||||
def delete(self, table_name: str, query: 'Query' = None) -> None:
|
||||
'''删除,query中只有query和fuzzy_query会被处理'''
|
||||
sql, sql_list = self.get_delete_sql(table_name, query)
|
||||
|
||||
@@ -243,8 +243,9 @@ class UserLogin(User):
|
||||
'name': self.name})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('Username does not exist.', 104)
|
||||
raise NoData(f'Username `{self.name}` does not exist.', 104)
|
||||
|
||||
self.user_id = x[0]
|
||||
self.now = int(time.time() * 1000)
|
||||
if x[2] is not None and x[2] != '':
|
||||
# 自动封号检查
|
||||
@@ -255,14 +256,14 @@ class UserLogin(User):
|
||||
|
||||
if x[1] == '':
|
||||
# 账号封禁
|
||||
raise UserBan('The account has been banned.', 106)
|
||||
raise UserBan(
|
||||
f'The account `{self.user_id}` has been banned.', 106)
|
||||
|
||||
if x[1] != self.hash_pwd:
|
||||
raise NoAccess('Wrong password.', 104)
|
||||
|
||||
self.user_id = str(x[0])
|
||||
self.token = base64.b64encode(hashlib.sha256(
|
||||
(self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
||||
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
||||
|
||||
self.c.execute(
|
||||
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
|
||||
|
||||
@@ -13,13 +13,11 @@ def md5(code: str) -> str:
|
||||
|
||||
|
||||
def get_file_md5(file_path: str) -> str:
|
||||
'''计算文件MD5'''
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
'''计算文件MD5,假设是文件'''
|
||||
myhash = hashlib.md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
b = f.read(8096)
|
||||
b = f.read(8192)
|
||||
if not b:
|
||||
break
|
||||
myhash.update(b)
|
||||
|
||||
@@ -50,6 +50,7 @@ class Step:
|
||||
self.restrict_id: str = None
|
||||
self.restrict_ids: list = []
|
||||
self.restrict_type: str = None
|
||||
self.restrict_difficulty: int = None
|
||||
self.step_type: list = None
|
||||
self.speed_limit_value: int = None
|
||||
self.plus_stamina_value: int = None
|
||||
@@ -61,12 +62,14 @@ class Step:
|
||||
}
|
||||
if self.items:
|
||||
r['items'] = [i.to_dict() for i in self.items]
|
||||
if self.restrict_id:
|
||||
r['restrict_id'] = self.restrict_id
|
||||
if self.restrict_ids:
|
||||
r['restrict_ids'] = self.restrict_ids
|
||||
if self.restrict_type:
|
||||
r['restrict_type'] = self.restrict_type
|
||||
if self.restrict_id:
|
||||
r['restrict_id'] = self.restrict_id
|
||||
if self.restrict_ids:
|
||||
r['restrict_ids'] = self.restrict_ids
|
||||
if self.restrict_difficulty is not None:
|
||||
r['restrict_difficulty'] = self.restrict_difficulty
|
||||
if self.step_type:
|
||||
r['step_type'] = self.step_type
|
||||
if self.speed_limit_value:
|
||||
@@ -82,6 +85,7 @@ class Step:
|
||||
self.restrict_id = d.get('restrict_id')
|
||||
self.restrict_ids = d.get('restrict_ids')
|
||||
self.restrict_type = d.get('restrict_type')
|
||||
self.restrict_difficulty = d.get('restrict_difficulty')
|
||||
self.step_type = d.get('step_type')
|
||||
self.speed_limit_value = d.get('speed_limit_value')
|
||||
self.plus_stamina_value = d.get('plus_stamina_value')
|
||||
@@ -102,7 +106,7 @@ class Map:
|
||||
self.available_from: int = None
|
||||
self.available_to: int = None
|
||||
self.is_repeatable: bool = None
|
||||
self.require_id: str = None
|
||||
self.require_id: 'str | list[str]' = None
|
||||
self.require_type: str = None
|
||||
self.require_value: int = None
|
||||
self.coordinate: str = None
|
||||
@@ -111,6 +115,9 @@ class Map:
|
||||
self.steps: list = []
|
||||
self.__rewards: list = None
|
||||
|
||||
self.require_localunlock_songid: str = None
|
||||
self.require_localunlock_challengeid: str = None
|
||||
|
||||
@property
|
||||
def rewards(self) -> list:
|
||||
if self.__rewards is None:
|
||||
@@ -151,6 +158,8 @@ class Map:
|
||||
'custom_bg': self.custom_bg,
|
||||
'stamina_cost': self.stamina_cost,
|
||||
'step_count': self.step_count,
|
||||
'require_localunlock_songid': self.require_localunlock_songid,
|
||||
'require_localunlock_challengeid': self.require_localunlock_challengeid,
|
||||
'steps': [s.to_dict() for s in self.steps],
|
||||
}
|
||||
|
||||
@@ -170,6 +179,8 @@ class Map:
|
||||
self.coordinate = raw_dict.get('coordinate')
|
||||
self.custom_bg = raw_dict.get('custom_bg', '')
|
||||
self.stamina_cost = raw_dict.get('stamina_cost')
|
||||
self.require_localunlock_songid = raw_dict.get('require_localunlock_songid', '')
|
||||
self.require_localunlock_challengeid = raw_dict.get('require_localunlock_challengeid', '')
|
||||
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
|
||||
return self
|
||||
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
class InitData:
|
||||
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
|
||||
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane']
|
||||
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)']
|
||||
|
||||
skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane']
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror']
|
||||
|
||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
|
||||
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0]
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0]
|
||||
|
||||
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32,
|
||||
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47]
|
||||
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 0, 57]
|
||||
|
||||
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52,
|
||||
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55]
|
||||
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 0, 70]
|
||||
|
||||
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18,
|
||||
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55]
|
||||
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 0, 57]
|
||||
|
||||
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52,
|
||||
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47]
|
||||
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50]
|
||||
|
||||
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73,
|
||||
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92]
|
||||
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80]
|
||||
|
||||
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
|
||||
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92]
|
||||
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50]
|
||||
|
||||
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
|
||||
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47]
|
||||
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50]
|
||||
|
||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
|
||||
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92]
|
||||
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80]
|
||||
|
||||
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
|
||||
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92]
|
||||
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50]
|
||||
|
||||
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2]
|
||||
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0]
|
||||
|
||||
char_core = {
|
||||
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||
@@ -64,7 +64,7 @@ class InitData:
|
||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase']
|
||||
|
||||
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3']
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle']
|
||||
|
||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
|
||||
@@ -75,12 +75,12 @@ class InitData:
|
||||
role_caption = ['系统', '管理员', '用户', '查询接口']
|
||||
|
||||
power = ['system', 'select', 'select_me', 'change', 'change_me', 'grant',
|
||||
'grant_inf', 'select_song_rank', 'select_song_info']
|
||||
'grant_inf', 'select_song_rank', 'select_song_info', 'select_song_rank_top']
|
||||
power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限',
|
||||
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限']
|
||||
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限', '歌曲排行榜有限查询权限']
|
||||
|
||||
role_power = {'system': power,
|
||||
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank', 'select_song_info'],
|
||||
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank_top', 'select_song_info'],
|
||||
'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'],
|
||||
'selector': ['select']
|
||||
}
|
||||
|
||||
25
latest version/database/init/log_tables.sql
Normal file
25
latest version/database/init/log_tables.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
create table if not exists cache(key text primary key,
|
||||
value text,
|
||||
expire_time int
|
||||
);
|
||||
create table if not exists user_score(user_id int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
time_played int,
|
||||
score int,
|
||||
shiny_perfect_count int,
|
||||
perfect_count int,
|
||||
near_count int,
|
||||
miss_count int,
|
||||
health int,
|
||||
modifier int,
|
||||
clear_type int,
|
||||
rating real,
|
||||
primary key(user_id, song_id, difficulty, time_played)
|
||||
);
|
||||
|
||||
create index if not exists user_score_1 on user_score (song_id, difficulty);
|
||||
create index if not exists user_score_2 on user_score (time_played);
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA default_cache_size = 4000;
|
||||
@@ -327,11 +327,10 @@
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 0,
|
||||
"price": 700,
|
||||
"orig_price": 700,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000,
|
||||
"discount_reason": "extend"
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "alice",
|
||||
@@ -595,6 +594,11 @@
|
||||
"id": "finale",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "epilogue",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
@@ -622,5 +626,25 @@
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
},
|
||||
{
|
||||
"name": "extend_2",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "extend_2",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 7,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 700,
|
||||
"price": 700,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
}
|
||||
]
|
||||
@@ -28,8 +28,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "yourvoiceso",
|
||||
@@ -48,8 +48,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "crosssoul",
|
||||
@@ -68,8 +68,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "impurebird",
|
||||
@@ -88,8 +88,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "auxesia",
|
||||
@@ -128,8 +128,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "yozakurafubuki",
|
||||
@@ -148,8 +148,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "surrender",
|
||||
@@ -168,8 +168,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "metallicpunisher",
|
||||
@@ -224,8 +224,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "callmyname",
|
||||
@@ -244,8 +244,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "fallensquare",
|
||||
@@ -264,8 +264,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "dropdead",
|
||||
@@ -304,8 +304,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "astraltale",
|
||||
@@ -324,8 +324,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "phantasia",
|
||||
@@ -380,8 +380,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "dottodot",
|
||||
@@ -400,8 +400,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "dreadnought",
|
||||
@@ -420,8 +420,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "mirzam",
|
||||
@@ -440,8 +440,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "heavenlycaress",
|
||||
@@ -460,8 +460,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "filament",
|
||||
@@ -480,8 +480,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "avantraze",
|
||||
@@ -518,8 +518,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "saikyostronger",
|
||||
@@ -556,8 +556,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "einherjar",
|
||||
@@ -576,8 +576,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "laqryma",
|
||||
@@ -596,8 +596,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "amygdata",
|
||||
@@ -616,8 +616,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "altale",
|
||||
@@ -636,8 +636,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "feelssoright",
|
||||
@@ -656,8 +656,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "scarletcage",
|
||||
@@ -712,8 +712,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "badtek",
|
||||
@@ -732,8 +732,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "maliciousmischance",
|
||||
@@ -788,8 +788,8 @@
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "xeraphinite",
|
||||
@@ -807,7 +807,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "xanatos",
|
||||
@@ -825,7 +827,9 @@
|
||||
}
|
||||
],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
"orig_price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "attraqtia",
|
||||
@@ -843,7 +847,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "gimmedablood",
|
||||
@@ -879,7 +885,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "theultimacy",
|
||||
@@ -897,7 +905,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "rekkaresonanc",
|
||||
@@ -945,7 +955,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "eveninginscarlet",
|
||||
@@ -981,7 +993,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "goldenslaughterer",
|
||||
@@ -999,7 +1013,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "redolentshape",
|
||||
@@ -1017,7 +1033,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
"price": 100,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "summerfireworks",
|
||||
@@ -1056,29 +1074,11 @@
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "internetoverdose",
|
||||
"name": "kissinglucifer",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "internetoverdose",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "macromod",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "macromod",
|
||||
"id": "kissinglucifer",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
@@ -1110,11 +1110,11 @@
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "kissinglucifer",
|
||||
"name": "macromod",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "kissinglucifer",
|
||||
"id": "macromod",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
@@ -1128,11 +1128,11 @@
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "headbonkache",
|
||||
"name": "internetoverdose",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "headbonkache",
|
||||
"id": "internetoverdose",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
@@ -1163,6 +1163,24 @@
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "headbonkache",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "headbonkache",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "picopicotranslation",
|
||||
"items": [
|
||||
@@ -1217,6 +1235,24 @@
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "cocorocosmetic",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "cocorocosmetic",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "freemyself",
|
||||
"items": [
|
||||
@@ -1236,11 +1272,29 @@
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "cocorocosmetic",
|
||||
"name": "capella",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "cocorocosmetic",
|
||||
"id": "capella",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "nullapophenia",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "nullapophenia",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ import server
|
||||
import web.index
|
||||
import web.login
|
||||
from core.constant import Constant
|
||||
from core.download import (UserDownload, initialize_songfile)
|
||||
from core.download import UserDownload
|
||||
from core.error import ArcError, NoAccess, RateLimit
|
||||
from core.init import FileChecker
|
||||
from core.sql import Connect
|
||||
@@ -105,6 +105,12 @@ if Config.DEPLOY_MODE == 'waitress':
|
||||
f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}')
|
||||
return response
|
||||
|
||||
# @app.before_request
|
||||
# def before_request():
|
||||
# print(request.path)
|
||||
# print(request.headers)
|
||||
# print(request.data)
|
||||
|
||||
|
||||
def tcp_server_run():
|
||||
if Config.DEPLOY_MODE == 'gevent':
|
||||
@@ -128,6 +134,18 @@ def tcp_server_run():
|
||||
app.run(Config.HOST, Config.PORT)
|
||||
|
||||
|
||||
def generate_log_file_dict(level: str, filename: str) -> dict:
|
||||
return {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": level,
|
||||
"formatter": "default",
|
||||
"filename": filename
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
log_dict = {
|
||||
'version': 1,
|
||||
@@ -141,15 +159,7 @@ def main():
|
||||
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||
'formatter': 'default'
|
||||
},
|
||||
"error_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "ERROR",
|
||||
"formatter": "default",
|
||||
"filename": "./log/error.log"
|
||||
}
|
||||
"error_file": generate_log_file_dict('ERROR', './log/error.log')
|
||||
},
|
||||
'formatters': {
|
||||
'default': {
|
||||
@@ -159,50 +169,30 @@ def main():
|
||||
}
|
||||
if Config.ALLOW_INFO_LOG:
|
||||
log_dict['root']['handlers'].append('info_file')
|
||||
log_dict['handlers']['info_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "INFO",
|
||||
"formatter": "default",
|
||||
"filename": "./log/info.log"
|
||||
}
|
||||
log_dict['handlers']['info_file'] = generate_log_file_dict(
|
||||
'INFO', './log/info.log')
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
log_dict['root']['handlers'].append('warning_file')
|
||||
log_dict['handlers']['warning_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "WARNING",
|
||||
"formatter": "default",
|
||||
"filename": "./log/warning.log"
|
||||
}
|
||||
log_dict['handlers']['warning_file'] = generate_log_file_dict(
|
||||
'WARNING', './log/warning.log')
|
||||
|
||||
dictConfig(log_dict)
|
||||
|
||||
if not FileChecker(app).check_before_run():
|
||||
app.logger.error('Something wrong. The server will not run.')
|
||||
Connect.logger = app.logger
|
||||
if not FileChecker(app.logger).check_before_run():
|
||||
app.logger.error('Some errors occurred. The server will not run.')
|
||||
input('Press ENTER key to exit.')
|
||||
sys.exit()
|
||||
|
||||
app.logger.info("Start to initialize song data...")
|
||||
try:
|
||||
initialize_songfile()
|
||||
app.logger.info('Complete!')
|
||||
except:
|
||||
app.logger.warning('Initialization error!')
|
||||
|
||||
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
||||
from linkplay_server import link_play
|
||||
process = [Process(target=link_play, args=(
|
||||
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
||||
[p.start() for p in process]
|
||||
app.logger.info("Link Play UDP server is running on " +
|
||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_UDP_PORT) + " ...")
|
||||
app.logger.info("Link Play TCP server is running on " +
|
||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_TCP_PORT) + " ...")
|
||||
app.logger.info(
|
||||
f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...")
|
||||
app.logger.info(
|
||||
f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...")
|
||||
tcp_server_run()
|
||||
[p.join() for p in process]
|
||||
else:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
cd /d %~dp0
|
||||
:: Develop server
|
||||
python -B main.py
|
||||
python -B main.py
|
||||
pause
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from core.sql import Connect
|
||||
|
||||
|
||||
def calculate_rating(defnum, score):
|
||||
# 计算rating
|
||||
if score >= 10000000:
|
||||
ptt = defnum + 2
|
||||
elif score < 9800000:
|
||||
ptt = defnum + (score-9500000) / 300000
|
||||
if ptt < 0 and defnum != -10:
|
||||
ptt = 0
|
||||
else:
|
||||
ptt = defnum + 1 + (score-9800000) / 200000
|
||||
|
||||
return ptt
|
||||
|
||||
|
||||
def refresh_all_score_rating():
|
||||
# 刷新所有best成绩的rating
|
||||
error = 'Unknown error.'
|
||||
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||
x = c.fetchall()
|
||||
|
||||
if x:
|
||||
song_list = [i[0] for i in x]
|
||||
with Connect() as c:
|
||||
c.execute('''update best_score set rating=0 where song_id not in ({0})'''.format(
|
||||
','.join(['?']*len(song_list))), tuple(song_list))
|
||||
for i in x:
|
||||
for j in range(0, 4):
|
||||
defnum = -10 # 没在库里的全部当做定数-10
|
||||
if i is not None:
|
||||
defnum = float(i[j+1]) / 10
|
||||
if defnum <= 0:
|
||||
defnum = -10 # 缺少难度的当做定数-10
|
||||
|
||||
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
|
||||
'a': i[0], 'b': j})
|
||||
y = c.fetchall()
|
||||
if y:
|
||||
for k in y:
|
||||
ptt = calculate_rating(defnum, k[1])
|
||||
if ptt < 0:
|
||||
ptt = 0
|
||||
|
||||
c.execute('''update best_score set rating=:a where user_id=:b and song_id=:c and difficulty=:d''', {
|
||||
'a': ptt, 'b': k[0], 'c': i[0], 'd': j})
|
||||
error = None
|
||||
|
||||
else:
|
||||
error = 'No song data.'
|
||||
|
||||
return error
|
||||
@@ -5,7 +5,7 @@ from core.config_manager import Config
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.sql import Connect
|
||||
from core.user import UserAuth, UserLogin
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask import Blueprint, g, jsonify, request
|
||||
|
||||
from .func import arc_try, error_return
|
||||
|
||||
@@ -16,10 +16,9 @@ bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
@arc_try
|
||||
def login():
|
||||
headers = request.headers
|
||||
if 'AppVersion' in headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Wrong app version.', 1203)
|
||||
if Config.ALLOW_APPVERSION: # 版本检查
|
||||
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Invalid app version.', 1203)
|
||||
|
||||
request.form['grant_type']
|
||||
with Connect() as c:
|
||||
@@ -45,16 +44,19 @@ def auth_required(request):
|
||||
|
||||
headers = request.headers
|
||||
|
||||
if 'AppVersion' in headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
return error_return(NoAccess('Wrong app version.', 1203))
|
||||
if Config.ALLOW_APPVERSION: # 版本检查
|
||||
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
return error_return(NoAccess('Invalid app version.', 1203))
|
||||
|
||||
with Connect() as c:
|
||||
try:
|
||||
user = UserAuth(c)
|
||||
user.token = headers['Authorization'][7:]
|
||||
token = headers.get('Authorization')
|
||||
if not token:
|
||||
raise NoAccess('No token.', -4)
|
||||
user.token = token[7:]
|
||||
user_id = user.token_get_id()
|
||||
g.user = user
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return view(user_id, *args, **kwargs)
|
||||
|
||||
@@ -3,7 +3,7 @@ from traceback import format_exc
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError
|
||||
from flask import current_app, jsonify
|
||||
from flask import current_app, g, jsonify
|
||||
|
||||
default_error = ArcError('Unknown Error', status=500)
|
||||
|
||||
@@ -83,6 +83,9 @@ def arc_try(view):
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
current_app.logger.warning(format_exc())
|
||||
user = g.get("user", None)
|
||||
current_app.logger.warning(
|
||||
f'{user.user_id if user is not None else ""} - {e.error_code}|{e.api_error_code}: {e}')
|
||||
return error_return(e)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import os
|
||||
from shutil import copy, copy2
|
||||
|
||||
from core.sql import Connect
|
||||
from database.database_initialize import ARCAEA_SERVER_VERSION, main
|
||||
from web.system import update_database
|
||||
|
||||
|
||||
def try_rename(path, new_path):
|
||||
# 尝试重命名文件,并尝试避免命名冲突,返回最终路径
|
||||
final_path = new_path
|
||||
if os.path.exists(new_path):
|
||||
i = 1
|
||||
while os.path.exists(new_path + str(i)):
|
||||
i += 1
|
||||
os.rename(path, new_path + str(i))
|
||||
final_path = new_path + str(i)
|
||||
else:
|
||||
os.rename(path, new_path)
|
||||
|
||||
return final_path
|
||||
|
||||
|
||||
def check_before_run(app):
|
||||
# 运行前检查关键文件,返回布尔值,其实是因为有人经常忘了
|
||||
|
||||
f = True
|
||||
|
||||
if not os.path.exists('database'):
|
||||
app.logger.warning('Folder `database` is missing.')
|
||||
f = False
|
||||
|
||||
if not os.path.exists('database/songs'):
|
||||
app.logger.warning('Folder `database/songs` is missing.')
|
||||
f = False
|
||||
|
||||
if not os.path.exists('database/arcaea_database.db'):
|
||||
app.logger.warning('File `database/arcaea_database.db` is missing.')
|
||||
f = False
|
||||
try:
|
||||
app.logger.info(
|
||||
'Try to new the file `database/arcaea_database.db`.')
|
||||
main('./database/')
|
||||
app.logger.info(
|
||||
'Success to new the file `database/arcaea_database.db`.')
|
||||
f = True
|
||||
except:
|
||||
app.logger.warning(
|
||||
'Fail to new the file `database/arcaea_database.db`.')
|
||||
|
||||
else:
|
||||
with Connect() as c:
|
||||
try:
|
||||
c.execute('''select value from config where id="version"''')
|
||||
x = c.fetchone()
|
||||
except:
|
||||
x = None
|
||||
|
||||
# 数据库自动更新,不强求
|
||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
||||
app.logger.warning(
|
||||
'Maybe the file `database/arcaea_database.db` is an old version.')
|
||||
try:
|
||||
app.logger.info(
|
||||
'Try to update the file `database/arcaea_database.db`.')
|
||||
|
||||
path = try_rename('database/arcaea_database.db',
|
||||
'database/arcaea_database.db.bak')
|
||||
|
||||
try:
|
||||
copy2(path, 'database/arcaea_database.db')
|
||||
except:
|
||||
copy(path, 'database/arcaea_database.db')
|
||||
|
||||
if os.path.isfile("database/old_arcaea_database.db"):
|
||||
os.remove('database/old_arcaea_database.db')
|
||||
|
||||
try_rename('database/arcaea_database.db',
|
||||
'database/old_arcaea_database.db')
|
||||
|
||||
main('./database/')
|
||||
update_database()
|
||||
|
||||
app.logger.info(
|
||||
'Success to update the file `database/arcaea_database.db`.')
|
||||
|
||||
except:
|
||||
app.logger.warning(
|
||||
'Fail to update the file `database/arcaea_database.db`.')
|
||||
|
||||
return f
|
||||
@@ -1,11 +1,10 @@
|
||||
from core.error import ArcError
|
||||
from core.present import UserPresent, UserPresentList
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, error_return, success_return
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('present', __name__, url_prefix='/present')
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ bp = Blueprint('user', __name__, url_prefix='/user')
|
||||
@bp.route('', methods=['POST']) # 注册接口
|
||||
@arc_try
|
||||
def register():
|
||||
if 'AppVersion' in request.headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Wrong app version.', 1203)
|
||||
headers = request.headers
|
||||
if Config.ALLOW_APPVERSION: # 版本检查
|
||||
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Invalid app version.', 1203)
|
||||
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import server.arcscore
|
||||
from core.download import DownloadList, initialize_songfile
|
||||
from core.init import FileChecker
|
||||
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
|
||||
from core.rank import RankList
|
||||
from core.sql import Connect
|
||||
from core.user import User
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@@ -290,8 +290,7 @@ def update_database():
|
||||
def update_song_hash():
|
||||
# 更新数据库内谱面文件hash值
|
||||
try:
|
||||
DownloadList.clear_all_cache()
|
||||
initialize_songfile()
|
||||
RefreshSongFileCache().run()
|
||||
flash('数据刷新成功 Success refresh data.')
|
||||
except:
|
||||
flash('Something error!')
|
||||
@@ -302,11 +301,8 @@ def update_song_hash():
|
||||
@login_required
|
||||
def update_song_rating():
|
||||
# 更新所有分数的rating
|
||||
error = server.arcscore.refresh_all_score_rating()
|
||||
if error:
|
||||
flash(error)
|
||||
else:
|
||||
flash('数据刷新成功 Success refresh data.')
|
||||
RefreshAllScoreRating().run()
|
||||
flash('数据刷新成功 Success refresh data.')
|
||||
return render_template('web/updatedatabase.html')
|
||||
|
||||
|
||||
@@ -426,7 +422,7 @@ def all_character():
|
||||
def change_character():
|
||||
# 修改角色数据
|
||||
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane']
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror']
|
||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||
|
||||
|
||||
@@ -610,7 +606,7 @@ def edit_user_purchase():
|
||||
if 'name' not in request.form and 'user_code' not in request.form:
|
||||
flag = False
|
||||
if method == '0':
|
||||
web.system.unlock_all_user_item(c)
|
||||
UnlockUserItem().run()
|
||||
else:
|
||||
c.execute(
|
||||
'''delete from user_item where type in ('pack', 'single')''')
|
||||
@@ -636,7 +632,9 @@ def edit_user_purchase():
|
||||
user_id = user_id[0]
|
||||
|
||||
if method == '0':
|
||||
web.system.unlock_user_item(c, user_id)
|
||||
x = UnlockUserItem()
|
||||
x.set_params(user_id=user_id)
|
||||
x.run()
|
||||
else:
|
||||
c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', {
|
||||
'user_id': user_id})
|
||||
@@ -942,7 +940,7 @@ def update_user_save():
|
||||
# 全修改
|
||||
if 'name' not in request.form and 'user_code' not in request.form:
|
||||
flag = False
|
||||
web.system.update_all_save(c)
|
||||
SaveUpdateScore().run()
|
||||
flash("全部用户存档同步成功 Successfully update all users' saves.")
|
||||
|
||||
else:
|
||||
@@ -962,7 +960,9 @@ def update_user_save():
|
||||
user_id = c.fetchone()
|
||||
if user_id:
|
||||
user_id = user_id[0]
|
||||
web.system.update_one_save(c, user_id)
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
SaveUpdateScore(user).run()
|
||||
flash("用户存档同步成功 Successfully update the user's saves.")
|
||||
|
||||
else:
|
||||
|
||||
@@ -40,41 +40,6 @@ def update_user_char(c):
|
||||
(j[0], i[0], i[1], exp, i[2], 0))
|
||||
|
||||
|
||||
def unlock_all_user_item(c):
|
||||
# 解锁所有用户购买
|
||||
|
||||
c.execute('''select user_id from user''')
|
||||
x = c.fetchall()
|
||||
c.execute('''select item_id, type from purchase_item''')
|
||||
y = c.fetchall()
|
||||
if x and y:
|
||||
for i in x:
|
||||
for j in y:
|
||||
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
|
||||
'a': i[0], 'b': j[0], 'c': j[1]})
|
||||
if c.fetchone() == (0,) and j[1] != 'character':
|
||||
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
|
||||
'a': i[0], 'b': j[0], 'c': j[1]})
|
||||
|
||||
return
|
||||
|
||||
|
||||
def unlock_user_item(c, user_id):
|
||||
# 解锁用户购买
|
||||
|
||||
c.execute('''select item_id, type from purchase_item''')
|
||||
y = c.fetchall()
|
||||
|
||||
for j in y:
|
||||
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
|
||||
'a': user_id, 'b': j[0], 'c': j[1]})
|
||||
if c.fetchone() == (0,) and j[1] != 'character':
|
||||
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
|
||||
'a': user_id, 'b': j[0], 'c': j[1]})
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_all_item():
|
||||
# 所有物品数据查询
|
||||
with Connect() as c:
|
||||
@@ -132,61 +97,6 @@ def get_all_purchase():
|
||||
return re
|
||||
|
||||
|
||||
def update_one_save(c, user_id):
|
||||
# 同步指定用户存档
|
||||
# 注意,best_score表不比较,直接覆盖
|
||||
return
|
||||
|
||||
# c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', {
|
||||
# 'a': user_id})
|
||||
# x = c.fetchone()
|
||||
# if x:
|
||||
# scores = json.loads(x[0])[""]
|
||||
# clearlamps = json.loads(x[1])[""]
|
||||
# clear_song_id_difficulty = []
|
||||
# clear_state = []
|
||||
# for i in clearlamps:
|
||||
# clear_song_id_difficulty.append(i['song_id']+str(i['difficulty']))
|
||||
# clear_state.append(i['clear_type'])
|
||||
|
||||
# for i in scores:
|
||||
# rating = server.arcscore.get_one_ptt(
|
||||
# i['song_id'], i['difficulty'], i['score'])
|
||||
# if rating < 0:
|
||||
# rating = 0
|
||||
# try:
|
||||
# index = clear_song_id_difficulty.index(
|
||||
# i['song_id'] + str(i['difficulty']))
|
||||
# except:
|
||||
# index = -1
|
||||
# if index != -1:
|
||||
# clear_type = clear_state[index]
|
||||
# else:
|
||||
# clear_type = 0
|
||||
# c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty']})
|
||||
# c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', {
|
||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating})
|
||||
|
||||
# ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT
|
||||
# c.execute('''update user set rating_ptt=:a where user_id=:b''', {
|
||||
# 'a': ptt, 'b': user_id})
|
||||
|
||||
# return
|
||||
|
||||
|
||||
def update_all_save(c):
|
||||
# 同步所有用户存档
|
||||
|
||||
c.execute('''select user_id from user_save''')
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
update_one_save(c, i[0])
|
||||
|
||||
return
|
||||
|
||||
|
||||
def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount):
|
||||
# 添加一个奖励
|
||||
|
||||
|
||||
Reference in New Issue
Block a user