Merge pull request #75 from Lost-MSth/dev

Update to v2.10.1
This commit is contained in:
Lost-MSth
2022-11-10 18:33:50 +08:00
committed by GitHub
17 changed files with 398 additions and 165 deletions

View File

@@ -67,39 +67,31 @@ It is just so interesting. What it can do is under exploration.
> 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.
### **2022.11.5** ### Version 2.10.1
**修复一长期BUG影响到自2022.7.4以来所有版本(表现为数据库锁以及服务端运行缓慢),请拉取或下载`master`分支最新提交** - 适用于Arcaea 4.1.4版本 For Arcaea 4.1.4
- 新搭档 **天音** 已解锁 Unlock the character **Amane**.
**Fix a long-term bug that affects all versions since Jul/4/2022 (shown as database lock and slow running). Please pull or download the latest commit from the `master` branch.** - 为**天音**技能提供支持 Add support for the skill of **Amane**.
- 现在配置文件可以是含有部分选项的文件或模块 At present the setting file can be a module or a file with some of options.
### Version 2.10.0 - 添加`waitress``gevent`的部署方案支持,并支持日志记录 Add deployment mode `waitress` and `gevent`, and add support for the info log recording of them.
- 适用于Arcaea 4.1.0版本 For Arcaea 4.1.0 - `songlist`添加解析器以指定可下载的文件 Add a parser for `songlist` to specify downloadable files.
- 新搭档 **咲姬** 已解锁 Unlock the character **Saki**. - 重构数据库初始化和数据迁移部分 Code refactoring for database initialization and migration.
- 新搭档 **刹那** 已解锁 Unlock the character **Setsuna**. - 限制用户下载频率将使用第三方限制器,替代数据库 Add a custom limiter and use it for limiting users' download rate instead of using database.
- 完善了日志系统 Improve the log system. > 现在需要`limits`模块
- 现在可以利用`songlist`确保`3.aff`以外文件不被下载 Now you can use `songlist` to ensure that files other than `3.aff` should not be downloaded. [#60](https://github.com/Lost-MSth/Arcaea-server/issues/60) > Now `limits` module is required.
- 适配v4.0.0以下版本的客户端云存档 Ensure that the clients under v4.0.0 can upload the cloud save. - 为登录和API登录添加限制器 Add limiter for login and API login.
- 优化数据库索引 Optimize database indices. - `sqlite3`数据库调整为WAL模式并增大缓存 Change journal mode to WAL and enlarge cache size for `sqlite3` database.
- 尝试确保LinkPlay服务器的线程安全现在此功能将作为独立服务端 Try to ensure thread safety in LinkPlay server. Now this function will be served as an independent server. - 将下载token放入内存中而不是文件数据库中 Put download token in memory database instead of filesystem database.
- 对API接口的分数列表添加歌曲名 Add song names for getting the score list in API. - 加速`best_score`表多次查询,表现为歌曲排行榜查询性能提升 Accelerate multiple querying in `best_score` table, which results in performance improvement of song ranklist query.
- 为下载错误增添HTTP状态码 Add HTTP status code when meeting download error. - 优化歌曲下载部分 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)
- 修复iOS客户端因世界模式地图数据闪退的问题 Fix a bug when world maps' data don't have some unnecessary parts the client of iOS may break down. - 修复课题模式成绩没有rating的问题 Fix a bug that scores in course mode cannot calculate rating.
-复API接口无数据`GET`请求导致报错的问题 Fix a bug that `GET` requests without data will report an error in API. [#50](https://github.com/Lost-MSth/Arcaea-server/issues/50) -正搭档数值 Fix a character's value.
- 修复`aggregate`请求无法捕获内部错误的问题 Fix a bug that `aggregate` requests will get an error when the inner function raises an error. - 邮箱长度最大限制提升到64 Change the email max length to 64.
- 修复因错误设置主键导致课程模式谱面无法相同的问题 Fix a bug that the charts of a course cannot be the same because of the incorrect primary keys. - 新增API接口来获取用户身份与权限 Add a method of API for getting users' roles and powers.
- 修复无谱面数据时世界排名分计算出错的问题 Fix a bug that global ranking scores cannot be calculated if there are no chart in the database. [#61](https://github.com/Lost-MSth/Arcaea-server/issues/61) - 新增API接口来修改用户信息 Add a method of API to change the user's info.
- 修复条件满足但隐藏段位依然无法解锁的问题 Fix a bug that the hidden courses cannot appear even if their requirements are satisfied. - 为API的`GET`请求添加`query`参数支持 Add support for the `query` param in API's `GET` requests.
- 修复Finale挑战中某些无法解锁的问题 Fix a bug that something of the finale challenge cannot be unlocked. - 修复API的`best30`接口曲目无数据导致出错的问题 Fix a bug that `best30` of API cannot have scores whose songs are not in database.
- 修复用户物品数量无法为0的问题此问题导致了一些购买操作异常 Fix a bug that the users' items will not be zero, which will disturb some purchase operations. - 修复API的`recent30`接口用户成绩数量不足导致出错的问题 Fix a bug that users with no recent scores cannot get `recent30` via API.
- 修复角色等级能超过最大等级的问题 Fix a bug that the level of the character can exceed the max level.
- 修复使用`以太之滴`升级角色时应答不正确的问题 Fix a bug that the response is incorrect when upgrading the characters by `generic core`.
- 修复`源韵强化`数值显示不正确的问题 Fix a bug that the `prog boost` shows the incorrect value.
- 修复世界模式奖励可能被重复发放的问题 Fix a bug that the rewards can be get repeatedly in World Mode.
- 修复世界Boss的第二管血量无法削减的问题 Fix a bug that second tube of blood of the world boss won't change.
- 修复我的排名显示不正确的问题 Fix a bug that `my rank` doesn't work correctly.
- 修复在歌曲结束后无法及时轮换房主的问题 Fix a bug that the room host will be changed late when finishing a song.
## 运行环境与依赖 Running environment and requirements ## 运行环境与依赖 Running environment and requirements
- Windows/Linux/Mac OS/Android... - Windows/Linux/Mac OS/Android...

View File

@@ -70,11 +70,11 @@ def request_json_handle(request, required_keys=[], optional_keys=[]):
json_data = loads( json_data = loads(
b64decode(request.args['query']).decode()) b64decode(request.args['query']).decode())
else: else:
return view(data, *args, **kwargs) json_data = {}
for key in required_keys: for key in required_keys:
if key not in json_data: if key not in json_data:
return error_return(PostError('Missing parameter: ' + key, api_error_code=-100)) return error_return(PostError(f'Missing parameter: {key}', api_error_code=-100))
data[key] = json_data[key] data[key] = json_data[key]
for key in optional_keys: for key in optional_keys:

View File

@@ -6,21 +6,23 @@ default_error = ArcError('Unknown Error')
CODE_MSG = { CODE_MSG = {
0: '', 0: '',
-1: 'See status code', -1: 'See status code', # 基础错误
-2: 'No data', -2: 'No data',
-3: 'No data or user', -3: 'No data or user', # 不确定是无数据还是无用户
-4: 'No user_id', -100: 'Invalid post data', # 1xx数据错误
-100: 'Wrong post data', -101: 'Invalid data type',
-101: 'Wrong data type', -102: 'Invalid query parameter',
-102: 'Wrong query parameter', -103: 'Invalid sort parameter',
-103: 'Wrong sort parameter', -104: 'Invalid sort order parameter',
-104: 'Wrong sort order parameter', -105: 'Invalid URL parameter',
-200: 'No permission', -110: 'Invalid user_id',
-200: 'No permission', # 2xx用户相关错误
-201: 'Wrong username or password', -201: 'Wrong username or password',
-202: 'User is banned', -202: 'User is banned',
-203: 'Username exists', -203: 'Too many login attempts',
-204: 'Email address exists', -210: 'Username exists',
-205: 'Too many login attempts', -211: 'Email address exists',
-212: 'User code exists',
-999: 'Unknown error' -999: 'Unknown error'
} }

View File

@@ -1,7 +1,7 @@
from core.error import InputError, NoAccess, NoData from core.error import InputError, NoAccess, NoData
from core.score import Potential, UserScoreList from core.score import Potential, UserScoreList
from core.sql import Connect, Query, Sql from core.sql import Connect, Query, Sql
from core.user import UserInfo, UserRegister from core.user import UserChanger, UserInfo, UserRegister
from core.api_user import APIUser from core.api_user import APIUser
from flask import Blueprint, request from flask import Blueprint, request
@@ -66,7 +66,7 @@ def users_get(data, user):
def users_user_get(user, user_id): def users_user_get(user, user_id):
'''查询用户信息''' '''查询用户信息'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
@@ -76,13 +76,45 @@ def users_user_get(user, user_id):
return success_return(u.to_dict()) return success_return(u.to_dict())
@bp.route('/<int:user_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'])
@api_try
def users_user_put(data, user, user_id):
'''修改一个用户'''
with Connect() as c:
u = UserChanger(c, user_id)
r = {}
r['user_id'] = user_id
if 'name' in data:
u.set_name(data['name'])
r['name'] = u.name
if 'password' in data:
u.set_password(data['password'])
r['password'] = u.hash_pwd
if 'email' in data:
u.set_email(data['email'])
r['email'] = u.email
if 'user_code' in data:
u.set_user_code(data['user_code'])
r['user_code'] = u.user_code
if 'ticket' in data:
if not isinstance(data['ticket'], int):
raise InputError('Ticket must be int')
u.ticket = data['ticket']
r['ticket'] = u.ticket
if r:
u.update_columns(d=r)
return success_return(r)
@bp.route('/<int:user_id>/b30', methods=['GET']) @bp.route('/<int:user_id>/b30', methods=['GET'])
@role_required(request, ['select', 'select_me']) @role_required(request, ['select', 'select_me'])
@api_try @api_try
def users_user_b30_get(user, user_id): def users_user_b30_get(user, user_id):
'''查询用户b30''' '''查询用户b30'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
@@ -91,6 +123,9 @@ def users_user_b30_get(user, user_id):
x = UserScoreList(c, UserInfo(c, user_id)) x = UserScoreList(c, UserInfo(c, user_id))
x.query.limit = 30 x.query.limit = 30
x.select_from_user() x.select_from_user()
if not x.scores:
raise NoData(
f'No best30 data of user `{user_id}`', api_error_code=-3)
x.select_song_name() x.select_song_name()
r = x.to_dict_list() r = x.to_dict_list()
rating_sum = sum([i.rating for i in x.scores]) rating_sum = sum([i.rating for i in x.scores])
@@ -104,7 +139,7 @@ def users_user_b30_get(user, user_id):
def users_user_best_get(data, user, user_id): def users_user_best_get(data, user, user_id):
'''查询用户所有best成绩''' '''查询用户所有best成绩'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
@@ -113,6 +148,9 @@ def users_user_best_get(data, user, user_id):
x = UserScoreList(c, UserInfo(c, user_id)) x = UserScoreList(c, UserInfo(c, user_id))
x.query.from_dict(data) x.query.from_dict(data)
x.select_from_user() x.select_from_user()
if not x.scores:
raise NoData(
f'No best score data of user `{user_id}`', api_error_code=-3)
r = x.to_dict_list() r = x.to_dict_list()
return success_return({'user_id': user_id, 'data': r}) return success_return({'user_id': user_id, 'data': r})
@@ -124,7 +162,7 @@ def users_user_r30_get(user, user_id):
'''查询用户r30''' '''查询用户r30'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
@@ -141,7 +179,7 @@ def users_user_role_get(user, user_id):
'''查询用户role和powers''' '''查询用户role和powers'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
if user_id == user.user_id: if user_id == user.user_id:
return success_return({'user_id': user.user_id, 'role': user.role.role_id, 'powers': [i.power_id for i in user.role.powers]}) return success_return({'user_id': user.user_id, 'role': user.role.role_id, 'powers': [i.power_id for i in user.role.powers]})

View File

@@ -1,6 +1,6 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.10.0.3' ARCAEA_SERVER_VERSION = 'v2.10.1'
class Constant: class Constant:
@@ -96,6 +96,6 @@ class Constant:
] ]
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save', DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char'] 'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -29,20 +29,69 @@ def initialize_songfile():
del x del x
@lru_cache() class SonglistParser:
def get_only_3_song_ids(): '''songlist文件解析器'''
'''初始化只能下载byd相关的歌曲id'''
if not os.path.isfile(Constant.SONGLIST_FILE_PATH): FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
return [] 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']
only_3_song_ids = [] songs: dict = {} # {song_id: value, ...}
data = [] # value: bit 76543210
with open(Constant.SONGLIST_FILE_PATH, 'r', encoding='utf-8') as f: # 7: video_audio.ogg
data = loads(f.read())['songs'] # 6: video.mp4
for x in data: # 5: 3.ogg
if 'remote_dl' not in x or 'remote_dl' in x and not x['remote_dl']: # 4: base.ogg
if any(i['ratingClass'] == 3 for i in x['difficulties']): # 3: 3.aff
only_3_song_ids.append(x['id']) # 2: 2.aff
return only_3_song_ids # 1: 1.aff
# 0: 0.aff
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
self.path = path
self.data: list = []
self.parse()
@staticmethod
def is_available_file(song_id: str, file_name: str) -> list:
'''判断文件是否允许被下载'''
if song_id not in SonglistParser.songs:
# songlist没有则只限制文件名
return file_name in SonglistParser.FILE_NAMES
rule = SonglistParser.songs[song_id]
for i in range(8):
if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0:
return True
return False
def parse_one(self, song: dict) -> dict:
'''解析单个歌曲'''
if not 'id' in song:
return None
r = 0
if 'remote_dl' in song and song['remote_dl']:
r |= 16
for i in song.get('difficulties', []):
if i['ratingClass'] == 3 and i.get('audioOverride', False):
r |= 32
r |= 1 << i['ratingClass']
else:
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
r |= 8
if 'additional_files' in song:
if 'video.mp4' in song['additional_files']:
r |= 64
if 'video_audio.ogg' in song['additional_files']:
r |= 128
return {song['id']: r}
def parse(self) -> None:
'''解析songlist文件'''
if not os.path.isfile(self.path):
return
with open(self.path, 'r', encoding='utf-8') as f:
self.data = loads(f.read()).get('songs', [])
for x in self.data:
self.songs.update(self.parse_one(x))
class UserDownload: class UserDownload:
@@ -99,7 +148,7 @@ class UserDownload:
def generate_token(self) -> None: def generate_token(self) -> None:
self.token_time = int(time()) self.token_time = int(time())
self.token = md5(str(self.user.user_id) + self.song_id + self.token = md5(str(self.user.user_id) + self.song_id +
self.file_name + str(self.token_time)) self.file_name + str(self.token_time) + str(os.urandom(8)))
def insert_download_token(self) -> None: def insert_download_token(self) -> None:
'''将数据插入数据库,让这个下载链接可用''' '''将数据插入数据库,让这个下载链接可用'''
@@ -111,14 +160,14 @@ class UserDownload:
'''生成下载链接''' '''生成下载链接'''
if self.token is None: if self.token is None:
self.generate_token() self.generate_token()
self.insert_download_token() # self.insert_download_token() # 循环插入速度慢改为executemany
if Constant.DOWNLOAD_LINK_PREFIX: if Constant.DOWNLOAD_LINK_PREFIX:
prefix = Constant.DOWNLOAD_LINK_PREFIX prefix = Constant.DOWNLOAD_LINK_PREFIX
if prefix[-1] != '/': if prefix[-1] != '/':
prefix += '/' prefix += '/'
return prefix + self.song_id + '/' + self.file_name + '?t=' + self.token return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
else: else:
return url_for('download', file_path=self.song_id + '/' + self.file_name, t=self.token, _external=True) return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
@property @property
def hash(self) -> str: def hash(self) -> str:
@@ -140,67 +189,98 @@ class DownloadList(UserDownload):
self.downloads: list = [] self.downloads: list = []
self.urls: dict = {} self.urls: dict = {}
@staticmethod
def clear_all_cache():
'''清除所有歌曲文件有关缓存'''
get_song_file_md5.cache_clear()
DownloadList.get_one_song_file_names.cache_clear()
DownloadList.get_all_song_ids.cache_clear()
SonglistParser()
def clear_download_token(self) -> None: def clear_download_token(self) -> None:
'''清除过期下载链接''' '''清除过期下载链接'''
self.c_m.execute('''delete from download_token where time<?''', self.c_m.execute('''delete from download_token where time<?''',
(int(time()) - Constant.DOWNLOAD_TIME_GAP_LIMIT,)) (int(time()) - Constant.DOWNLOAD_TIME_GAP_LIMIT,))
def insert_download_tokens(self) -> None:
'''插入所有下载链接'''
self.c_m.executemany('''insert or replace into download_token values(?,?,?,?,?)''', [(
self.user.user_id, x.song_id, x.file_name, x.token, x.token_time) for x in self.downloads])
@staticmethod
@lru_cache(maxsize=2048)
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)
return r
def add_one_song(self, song_id: str) -> None: def add_one_song(self, song_id: str) -> None:
dir_list = os.listdir(os.path.join(
Constant.SONG_FILE_FOLDER_PATH, song_id))
re = {} re = {}
for i in dir_list: for i in self.get_one_song_file_names(song_id):
if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']: x = UserDownload(self.c_m, self.user)
if song_id in get_only_3_song_ids() and i not in ['3.aff', '3.ogg']: self.downloads.append(x)
continue x.song_id = song_id
x = UserDownload(self.c_m, self.user) x.file_name = i
# self.downloads.append(x) # 这实际上没有用 if i == 'base.ogg':
x.song_id = song_id if 'audio' not in re:
x.file_name = i re['audio'] = {}
if i == 'base.ogg':
if 'audio' not in re:
re['audio'] = {}
re['audio']["checksum"] = x.hash re['audio']["checksum"] = x.hash
if self.url_flag: if self.url_flag:
re['audio']["url"] = x.url re['audio']["url"] = x.url
elif i == '3.ogg': elif i == '3.ogg':
if 'audio' not in re: if 'audio' not in re:
re['audio'] = {} re['audio'] = {}
if self.url_flag: if self.url_flag:
re['audio']['3'] = {"checksum": x.hash, "url": x.url} re['audio']['3'] = {"checksum": x.hash, "url": x.url}
else:
re['audio']['3'] = {"checksum": x.hash}
elif i == 'video.mp4' or i == 'video_audio.ogg':
if 'additional_files' not in re:
re['additional_files'] = []
if self.url_flag:
re['additional_files'].append(
{"checksum": x.hash, "url": x.url, 'file_name': i})
else:
re['additional_files'].append(
{"checksum": x.hash, 'file_name': i})
else: else:
if 'chart' not in re: re['audio']['3'] = {"checksum": x.hash}
re['chart'] = {} elif i == 'video.mp4' or i == 'video_audio.ogg':
if 'additional_files' not in re:
re['additional_files'] = []
if self.url_flag: if self.url_flag:
re['chart'][i[0]] = {"checksum": x.hash, "url": x.url} re['additional_files'].append(
else: {"checksum": x.hash, "url": x.url, 'file_name': i})
re['chart'][i[0]] = {"checksum": x.hash} else:
re['additional_files'].append(
{"checksum": x.hash, 'file_name': i})
else:
if 'chart' not in re:
re['chart'] = {}
if self.url_flag:
re['chart'][i[0]] = {"checksum": x.hash, "url": x.url}
else:
re['chart'][i[0]] = {"checksum": x.hash}
self.urls.update({song_id: re}) self.urls.update({song_id: re})
@staticmethod
@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)))
def add_songs(self, song_ids: list = None) -> None: def add_songs(self, song_ids: list = None) -> None:
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲''' '''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
if song_ids is not None: if song_ids is not None:
self.song_ids = song_ids self.song_ids = song_ids
x = self.song_ids if self.song_ids else os.listdir( if not self.song_ids:
Constant.SONG_FILE_FOLDER_PATH) self.song_ids = self.get_all_song_ids()
for i in x: for i in self.song_ids:
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
self.add_one_song(i) self.add_one_song(i)
else:
for i in self.song_ids:
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
self.add_one_song(i)
if self.url_flag:
self.clear_download_token()
self.insert_download_tokens()

View File

@@ -8,6 +8,7 @@ from time import time
from core.config_manager import Config from core.config_manager import Config
from core.constant import ARCAEA_SERVER_VERSION from core.constant import ARCAEA_SERVER_VERSION
from core.course import Course from core.course import Course
from core.download import SonglistParser
from core.purchase import Purchase from core.purchase import Purchase
from core.sql import Connect, DatabaseMigrator, MemoryDatabase from core.sql import Connect, DatabaseMigrator, MemoryDatabase
from core.user import UserRegister from core.user import UserRegister
@@ -241,5 +242,7 @@ class FileChecker:
def check_before_run(self) -> bool: def check_before_run(self) -> bool:
'''运行前检查,返回布尔值''' '''运行前检查,返回布尔值'''
# TODO: try
MemoryDatabase() # 初始化内存数据库 MemoryDatabase() # 初始化内存数据库
SonglistParser() # 解析songlist
return self.check_folder(Config.SONG_FILE_FOLDER_PATH) & self.check_update_database() return self.check_folder(Config.SONG_FILE_FOLDER_PATH) & self.check_update_database()

View File

@@ -155,7 +155,7 @@ class UserScore(Score):
super().__init__() super().__init__()
self.c = c self.c = c
self.user = user self.user = user
self.rank = None # 成绩排名给Ranklist用的 self.rank = None # 成绩排名给Ranklist用的
def select_score(self) -> None: def select_score(self) -> None:
'''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询''' '''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询'''
@@ -269,8 +269,11 @@ class UserPlay(UserScore):
'''select * from songplay_token where token=:a ''', {'a': self.song_token}) '''select * from songplay_token where token=:a ''', {'a': self.song_token})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No token data.') self.is_world_mode = False
self.song.set_chart(x[2], x[3]) self.course_play_state = -1
return None
# raise NoData('No token data.')
# self.song.set_chart(x[2], x[3])
if x[4]: if x[4]:
self.course_play = CoursePlay(self.c, self.user, self) self.course_play = CoursePlay(self.c, self.user, self)
self.course_play.course_id = x[4] self.course_play.course_id = x[4]
@@ -492,6 +495,9 @@ class Potential:
self.c.execute( self.c.execute(
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id}) '''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x:
raise NoData(
f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3)
self.r30 = [] self.r30 = []
self.s30 = [] self.s30 = []
if not x: if not x:
@@ -526,11 +532,15 @@ class Potential:
def recent_30_to_dict_list(self) -> list: def recent_30_to_dict_list(self) -> list:
if self.r30 is None: if self.r30 is None:
self.select_recent_30() self.select_recent_30()
return [{ r = []
'song_id': self.s30[i][:-1], for x, y in zip(self.s30, self.r30):
'difficulty': int(self.s30[i][-1]), if x:
'rating': self.r30[i] r.append({
} for i in range(len(self.r30))] 'song_id': x[:-1],
'difficulty': int(x[-1]),
'rating': y
})
return r
def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None: def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None:
self.r30.pop(pop_index) self.r30.pop(pop_index)

View File

@@ -215,6 +215,36 @@ class Sql:
'replace', 'R', 'r', 'REPLACE'] else 'ignore' 'replace', 'R', 'r', 'REPLACE'] else 'ignore'
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)) + ')' 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:
if not d:
return None
sql_list = []
sql = f"update {table_name} set {','.join([f'{k}=?' for k in d.keys()])}"
sql_list.extend(d.values())
if query is None:
return sql, sql_list
where_key = []
for k, v in query.query.items():
if isinstance(v, list):
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
sql_list.extend(v)
else:
where_key.append(f'{k}=?')
sql_list.append(v)
for k, v in query.fuzzy_query.items():
where_key.append(f'{k} like ?')
sql_list.append(f'%{v}%')
if where_key:
sql += ' where '
sql += ' and '.join(where_key)
return sql, sql_list
@staticmethod @staticmethod
def get_delete_sql(table_name: str, query: 'Query' = None): def get_delete_sql(table_name: str, query: 'Query' = None):
'''拼接删除语句query中只有query和fuzzy_query会被处理''' '''拼接删除语句query中只有query和fuzzy_query会被处理'''
@@ -267,6 +297,13 @@ class Sql:
self.c.executemany(self.get_insert_sql( self.c.executemany(self.get_insert_sql(
table_name, key, len(value_list[0]), insert_type), value_list) table_name, key, len(value_list[0]), insert_type), value_list)
def update(self, table_name: str, d: dict, query: 'Query' = None) -> None:
'''单表内行update单句sql语句'''
if not d:
return
sql, sql_list = self.get_update_sql(table_name, d, query)
self.c.execute(sql, sql_list)
def delete(self, table_name: str, query: 'Query' = None) -> None: def delete(self, table_name: str, query: 'Query' = None) -> None:
'''删除query中只有query和fuzzy_query会被处理''' '''删除query中只有query和fuzzy_query会被处理'''
sql, sql_list = self.get_delete_sql(table_name, query) sql, sql_list = self.get_delete_sql(table_name, query)

View File

@@ -11,7 +11,7 @@ from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
from .item import UserItemList from .item import UserItemList
from .limiter import ArcLimiter from .limiter import ArcLimiter
from .score import Score from .score import Score
from .sql import Connect from .sql import Query, Sql
from .world import Map, UserMap, UserStamina from .world import Map, UserMap, UserStamina
@@ -32,17 +32,17 @@ def code_get_id(c, user_code: str) -> int:
class User: class User:
def __init__(self) -> None: def __init__(self) -> None:
self.name = None self.name: str = None
self.email = None self.email: str = None
self.password = None self.password: str = None
self.user_id = None self.user_id: int = None
self.user_code = None self.user_code: str = None
self.join_date = None self.join_date = None
self.rating_ptt: int = None # 100 times self.rating_ptt: int = None # 100 times
self.ticket = None self.ticket: int = None
self.world_rank_score = None self.world_rank_score: int = None
self.ban_flag = None self.ban_flag = None
@property @property
@@ -63,7 +63,7 @@ class UserRegister(User):
if self.c.fetchone() == (0,): if self.c.fetchone() == (0,):
self.name = name self.name = name
else: else:
raise DataExist('Username exists.', 101, -203) raise DataExist('Username exists.', 101, -210)
else: else:
raise InputError('Username is invalid.') raise InputError('Username is invalid.')
@@ -82,10 +82,22 @@ class UserRegister(User):
if self.c.fetchone() == (0,): if self.c.fetchone() == (0,):
self.email = email self.email = email
else: else:
raise DataExist('Email address exists.', 102, -204) raise DataExist('Email address exists.', 102, -211)
else: else:
raise InputError('Email address is invalid.') raise InputError('Email address is invalid.')
def set_user_code(self, user_code: str) -> None:
'''设置用户的user_code'''
if len(user_code) == 9 and user_code.isdigit():
self.c.execute(
'''select exists(select * from user where user_code = ?)''', (user_code, ))
if self.c.fetchone() == (0,):
self.user_code = user_code
else:
raise DataExist('User code exists.', 103, -212)
else:
raise InputError('User code is invalid.')
def _build_user_code(self): def _build_user_code(self):
# 生成9位的user_code用的自然是随机 # 生成9位的user_code用的自然是随机
from random import randint from random import randint
@@ -225,7 +237,7 @@ class UserLogin(User):
self.set_ip(ip) self.set_ip(ip)
if not self.limiter.hit(name): if not self.limiter.hit(name):
raise RateLimit('Too many login attempts.', 123) raise RateLimit('Too many login attempts.', 123, -203)
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', { self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': self.name}) 'name': self.name})
@@ -283,7 +295,7 @@ class UserAuth(User):
class UserInfo(User): class UserInfo(User):
def __init__(self, c, user_id=None) -> None: def __init__(self, c, user_id=None) -> None:
super().__init__() User.__init__(self)
self.c = c self.c = c
self.user_id = user_id self.user_id = user_id
self.character = None self.character = None
@@ -733,3 +745,21 @@ class UserOnline(UserInfo):
self.favorite_character = UserCharacter(self.c, character_id, self) self.favorite_character = UserCharacter(self.c, character_id, self)
self.c.execute('''update user set favorite_character = :a where user_id = :b''', self.c.execute('''update user set favorite_character = :a where user_id = :b''',
{'a': self.favorite_character.character_id, 'b': self.user_id}) {'a': self.favorite_character.character_id, 'b': self.user_id})
class UserChanger(UserInfo, UserRegister):
def __init__(self, c, user_id=None) -> None:
super().__init__(c, user_id)
def update_columns(self, columns: list = None, d: dict = None) -> None:
if columns is not None:
d = {}
for column in columns:
if column == 'password':
d[column] = self.hash_pwd
else:
d[column] = self.__dict__[column]
Sql(self.c).update('user', d, Query().from_args(
{'user_id': self.user_id}))

View File

@@ -477,7 +477,7 @@ class WorldPlay:
if self.character_bonus_progress is not None: if self.character_bonus_progress is not None:
# 猜的,为了让客户端正确显示,当然结果是没问题的 # 猜的,为了让客户端正确显示,当然结果是没问题的
r['base_progress'] += self.character_bonus_progress # r['base_progress'] += self.character_bonus_progress # 肯定不是这样的
r['character_bonus_progress'] = self.character_bonus_progress r['character_bonus_progress'] = self.character_bonus_progress
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
@@ -593,7 +593,7 @@ class WorldPlay:
def after_climb(self) -> None: def after_climb(self) -> None:
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap, factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis} 'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane}
if self.character_used.skill_id_displayed in factory_dict: if self.character_used.skill_id_displayed in factory_dict:
factory_dict[self.character_used.skill_id_displayed]() factory_dict[self.character_used.skill_id_displayed]()
@@ -666,3 +666,14 @@ class WorldPlay:
self.user.world_mode_locked_end_ts = int( self.user.world_mode_locked_end_ts = int(
time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME
self.user.update_user_one_column('world_mode_locked_end_ts') self.user.update_user_one_column('world_mode_locked_end_ts')
def _skill_amane(self) -> None:
'''
amane技能起始格为限速或随机成绩小于EX时世界模式进度减半
偷懒在after_climb里面需要重爬一次
'''
x: 'Step' = self.user.current_map.steps_for_climbing[0]
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
self.character_bonus_progress = -self.step_value / 2 / self.step_times
self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value)

View File

@@ -1,45 +1,45 @@
class InitData: 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', 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'] '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']
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', 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'] '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_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', 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, 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, 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]
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, 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] 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]
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, 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] 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]
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, 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] 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]
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, 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] 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]
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, 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] 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]
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, 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] 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]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, 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] 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]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, 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] 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]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, 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] 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]
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, 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] 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]
char_core = { char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 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'] '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", 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'] "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']
world_unlocks = ["scenery_chap1", "scenery_chap2", world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]

View File

@@ -1216,5 +1216,41 @@
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100
},
{
"name": "freemyself",
"items": [
{
"type": "single",
"id": "freemyself",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"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
} }
] ]

View File

@@ -27,8 +27,7 @@ import server
import web.index import web.index
import web.login import web.login
from core.constant import Constant from core.constant import Constant
from core.download import (UserDownload, get_only_3_song_ids, from core.download import (UserDownload, initialize_songfile)
initialize_songfile)
from core.error import ArcError, NoAccess, RateLimit from core.error import ArcError, NoAccess, RateLimit
from core.init import FileChecker from core.init import FileChecker
from core.sql import Connect from core.sql import Connect
@@ -103,7 +102,7 @@ if Config.DEPLOY_MODE == 'waitress':
@app.after_request @app.after_request
def after_request(response): def after_request(response):
app.logger.info( app.logger.info(
f'B {request.remote_addr} - - {request.method} {request.path} {response.status_code}') f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}')
return response return response
@@ -116,8 +115,8 @@ def tcp_server_run():
WSGIServer(host_port, app, log=app.logger).serve_forever() WSGIServer(host_port, app, log=app.logger).serve_forever()
elif Config.DEPLOY_MODE == 'waitress': elif Config.DEPLOY_MODE == 'waitress':
# waitress WSGI server # waitress WSGI server
from waitress import serve
import logging import logging
from waitress import serve
logger = logging.getLogger('waitress') logger = logging.getLogger('waitress')
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
serve(app, host=Config.HOST, port=Config.PORT) serve(app, host=Config.HOST, port=Config.PORT)
@@ -191,7 +190,6 @@ def main():
app.logger.info("Start to initialize song data...") app.logger.info("Start to initialize song data...")
try: try:
initialize_songfile() initialize_songfile()
get_only_3_song_ids()
app.logger.info('Complete!') app.logger.info('Complete!')
except: except:
app.logger.warning('Initialization error!') app.logger.warning('Initialization error!')

View File

@@ -1,6 +1,5 @@
from core.constant import Constant from core.constant import Constant
from core.course import UserCourseList from core.course import UserCourseList
from core.error import ArcError
from core.item import ItemCore from core.item import ItemCore
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline

View File

@@ -35,8 +35,6 @@ def download_song(user_id):
x.url_flag = json.loads(request.args.get('url', 'true')) x.url_flag = json.loads(request.args.get('url', 'true'))
if x.url_flag and x.is_limited: if x.url_flag and x.is_limited:
raise RateLimit('You have reached the download limit.', 903) raise RateLimit('You have reached the download limit.', 903)
if x.url_flag:
x.clear_download_token()
x.add_songs() x.add_songs()
return success_return(x.urls) return success_return(x.urls)

View File

@@ -2,7 +2,7 @@ import os
import time import time
import server.arcscore import server.arcscore
from core.download import get_only_3_song_ids, initialize_songfile from core.download import DownloadList, initialize_songfile
from core.init import FileChecker from core.init import FileChecker
from core.rank import RankList from core.rank import RankList
from core.sql import Connect from core.sql import Connect
@@ -290,8 +290,7 @@ def update_database():
def update_song_hash(): def update_song_hash():
# 更新数据库内谱面文件hash值 # 更新数据库内谱面文件hash值
try: try:
get_only_3_song_ids.cache_clear() DownloadList.clear_all_cache()
get_only_3_song_ids()
initialize_songfile() initialize_songfile()
flash('数据刷新成功 Success refresh data.') flash('数据刷新成功 Success refresh data.')
except: except:
@@ -427,7 +426,7 @@ def all_character():
def change_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', 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'] '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']
return render_template('web/changechar.html', skill_ids=skill_ids) return render_template('web/changechar.html', skill_ids=skill_ids)