mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-10 01:37:27 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a4ff11578 | ||
|
|
18e79a7799 | ||
|
|
e548d5639f | ||
|
|
bd74d96250 | ||
|
|
880b66a995 | ||
|
|
8efd11cd02 |
81
README.md
81
README.md
@@ -17,42 +17,53 @@ This procedure is mainly used for study and research, and shall not be used for
|
|||||||
|
|
||||||
## 特性 Features
|
## 特性 Features
|
||||||
|
|
||||||
有以下 We have:
|
:x: : 不支持 Not supported
|
||||||
|
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
|
||||||
|
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
|
||||||
|
:construction: : 建设中 In construction
|
||||||
|
|
||||||
- 登录、注册 Login and registration
|
- 登录、注册 Login and registration
|
||||||
- 多设备登录 Multi device login
|
- 多设备自动封号 Auto-ban of multiple devices
|
||||||
|
- :warning: 多设备登录 Multi device login
|
||||||
|
- 登录频次限制 Login rate limit
|
||||||
|
- :x: 销号 Destroy account
|
||||||
- 成绩上传 Score upload
|
- 成绩上传 Score upload
|
||||||
- PTT
|
- 成绩校验 Score check
|
||||||
- 世界排名 Global rank
|
- 成绩排名 Score rank
|
||||||
- 排名 Rank
|
- 潜力值机制 Potential
|
||||||
|
- Best 30
|
||||||
|
- :warning: Recent Top 10
|
||||||
|
- :warning: 世界排名 Global rank
|
||||||
- 段位系统 Course system
|
- 段位系统 Course system
|
||||||
- Link Play
|
- :warning: Link Play
|
||||||
- 好友系统 Friends
|
- 好友系统 Friends
|
||||||
|
- :x: 好友位提升 Max friend number increase
|
||||||
- 云端存档 Cloud save
|
- 云端存档 Cloud save
|
||||||
- 爬梯 Climbing steps
|
- 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
|
||||||
- 自定义世界模式 Customizable World Mode
|
- 世界模式 World mode
|
||||||
- 自定义歌曲下载 Customizable songs download
|
- 体力系统 Stamina system
|
||||||
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
|
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
|
||||||
|
- :warning: 角色技能 Character skills
|
||||||
|
- 歌曲下载 Songs downloading
|
||||||
|
- :x: 加密下载 Encrypted downloading
|
||||||
|
- 下载校验 Download check
|
||||||
|
- 下载频次限制 Download rate limit
|
||||||
|
- 购买系统 Purchase system
|
||||||
|
- 单曲和曲包 Single & Pack
|
||||||
|
- :x: 捆绑包 Bundle
|
||||||
|
- 折扣 Discount
|
||||||
|
- 五周年兑换券 5-th anniversary ticket
|
||||||
|
- :x: Extend 包自动降价 Extend pack automatic price reduction
|
||||||
- 奖励系统 Present system
|
- 奖励系统 Present system
|
||||||
- 兑换码系统 Redeem code system
|
- 兑换码系统 Redeem code system
|
||||||
- 角色系统 Character system
|
- 角色系统 Character system
|
||||||
- 全剧情解锁 Unlock all the stories
|
- 数据记录 Data recording
|
||||||
- 后台查询 Background search
|
- 用户成绩 Users' scores
|
||||||
- 后台自定义信息 Customize some things in the background
|
- 用户每日潜力值 Users' daily potential
|
||||||
- 成绩校验 Score check
|
- :wastebasket: 简单的网页管理后台 Simple web admin backend
|
||||||
- 下载校验 Download check
|
- :construction: API
|
||||||
- 服务器日志 Server log
|
- 服务器日志 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
|
## 说明 Statement
|
||||||
|
|
||||||
只是很有趣,用处探索中。
|
只是很有趣,用处探索中。
|
||||||
@@ -70,21 +81,27 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
只保留最新版本 Only keep the latest version.
|
只保留最新版本 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.
|
||||||
|
>
|
||||||
|
> 其它小改动请参考各个 commit 信息
|
||||||
|
> Please refer to the commit messages for other minor changes.
|
||||||
|
|
||||||
### Version 2.11.1
|
### Version 2.11.2
|
||||||
|
|
||||||
- 适用于Arcaea 4.4.0版本 For Arcaea 4.4.0
|
- 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6
|
||||||
- 新搭档 **密特拉·泰尔塞拉**、**不来方斗亚** 已解锁 Unlock the character **Mithra Tercera** and **Toa Kozukata**.
|
- 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**.
|
||||||
- 为 **密特拉·泰尔塞拉** 的技能提供支持 Add support for the skill of **Mithra Tercera**.
|
- 新增用户潜力值每日记录功能 Add support for recording users' potential each day.
|
||||||
- 新增修改搭档的API接口 Add some API endpoints about characters.
|
- 修复搭档 **光 & 对立(Reunion)** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100)
|
||||||
|
- 添加 `finale/finale_end` 接口尝试修复最终挑战无法解锁结局的问题 Add the `finale/finale_end` endpoint to try to fix the problem that the endings cannot be unlocked correctly in the finale challenge. (#110)
|
||||||
|
- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records.
|
||||||
|
|
||||||
## 运行环境与依赖 Running environment and requirements
|
## 运行环境与依赖 Running environment and requirements
|
||||||
|
|
||||||
- Windows / Linux / Mac OS / Android...
|
- Windows / Linux / Mac OS / Android...
|
||||||
- Python >= 3.6
|
- Python >= 3.6
|
||||||
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
|
- Flask >= 2.0
|
||||||
|
- Cryptography >= 3.0.0
|
||||||
|
- limits >= 2.7.0
|
||||||
- Charles, IDA, proxy app... (optional)
|
- Charles, IDA, proxy app... (optional)
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.api_user import APIUser
|
from core.api_user import APIUser
|
||||||
from core.error import PostError
|
from core.error import PostError
|
||||||
@@ -32,6 +32,7 @@ def token_post(data):
|
|||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
user = APIUser(c)
|
user = APIUser(c)
|
||||||
user.login(name, password, request.remote_addr)
|
user.login(name, password, request.remote_addr)
|
||||||
|
current_app.logger.info(f'API user `{user.user_id}` log in')
|
||||||
return success_return({'token': user.api_token, 'user_id': user.user_id})
|
return success_return({'token': user.api_token, 'user_id': user.user_id})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
|
|
||||||
from core.api_user import APIUser
|
from core.api_user import APIUser
|
||||||
|
from core.config_manager import Config
|
||||||
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 UserChanger, UserInfo, UserRegister
|
from core.user import UserChanger, UserInfo, UserRegister
|
||||||
|
from core.util import get_today_timestamp
|
||||||
|
|
||||||
from .api_auth import api_try, request_json_handle, role_required
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
from .api_code import error_return, success_return
|
from .api_code import error_return, success_return
|
||||||
@@ -191,3 +193,32 @@ def users_user_role_get(user, user_id):
|
|||||||
x = APIUser(c, user_id)
|
x = APIUser(c, user_id)
|
||||||
x.select_role_and_powers()
|
x.select_role_and_powers()
|
||||||
return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
|
return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>/rating', methods=['GET'])
|
||||||
|
@role_required(request, ['select', 'select_me'])
|
||||||
|
@request_json_handle(request, optional_keys=['start_timestamp', 'end_timestamp', 'duration'])
|
||||||
|
@api_try
|
||||||
|
def users_user_rating_get(data, user, user_id):
|
||||||
|
'''查询用户历史rating,`duration`是相对于今天的天数'''
|
||||||
|
# 查别人需要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)
|
||||||
|
|
||||||
|
start_timestamp = data.get('start_timestamp', None)
|
||||||
|
end_timestamp = data.get('end_timestamp', None)
|
||||||
|
duration = data.get('duration', None)
|
||||||
|
sql = '''select time, rating_ptt from user_rating where user_id = ?'''
|
||||||
|
sql_data = [user_id]
|
||||||
|
if start_timestamp is not None and end_timestamp is not None:
|
||||||
|
sql += ''' and time between ? and ?'''
|
||||||
|
sql_data += [start_timestamp, end_timestamp]
|
||||||
|
elif duration is not None:
|
||||||
|
sql += ''' and time between ? and ?'''
|
||||||
|
t = get_today_timestamp()
|
||||||
|
sql_data += [t - duration * 24 * 3600, t]
|
||||||
|
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.execute(sql, sql_data)
|
||||||
|
r = c.fetchall()
|
||||||
|
return success_return({'user_id': user_id, 'data': [{'time': i[0], 'rating_ptt': i[1]} for i in r]})
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ class APIUser(UserOnline):
|
|||||||
if ip is not None:
|
if ip is not None:
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
if not self.limiter.hit(name):
|
if not self.limiter.hit(name):
|
||||||
raise RateLimit('Too many login attempts', api_error_code=-205)
|
raise RateLimit(
|
||||||
|
f'Too many login attempts of username {name}', api_error_code=-205)
|
||||||
|
|
||||||
self.c.execute('''select user_id, password from user where name = :a''', {
|
self.c.execute('''select user_id, password from user where name = :a''', {
|
||||||
'a': self.name})
|
'a': self.name})
|
||||||
@@ -136,9 +137,9 @@ class APIUser(UserOnline):
|
|||||||
raise NoData(
|
raise NoData(
|
||||||
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
|
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
|
||||||
if x[1] == '':
|
if x[1] == '':
|
||||||
raise UserBan(f'The user `{self.name}` is banned.')
|
raise UserBan(f'The user `{x[0]}` is banned.')
|
||||||
if self.hash_pwd != x[1]:
|
if self.hash_pwd != x[1]:
|
||||||
raise NoAccess('The password is incorrect.',
|
raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
|
||||||
api_error_code=-201, status=401)
|
api_error_code=-201, status=401)
|
||||||
|
|
||||||
self.user_id = x[0]
|
self.user_id = x[0]
|
||||||
|
|||||||
50
latest version/core/bgtask.py
Normal file
50
latest version/core/bgtask.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from atexit import register
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from .constant import Constant
|
||||||
|
from .sql import Connect
|
||||||
|
|
||||||
|
|
||||||
|
class BGTask:
|
||||||
|
executor = ThreadPoolExecutor(max_workers=1)
|
||||||
|
|
||||||
|
def __init__(self, func, *args, **kwargs):
|
||||||
|
self.future = self.executor.submit(func, *args, **kwargs)
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
return self.future.result()
|
||||||
|
|
||||||
|
def cancel(self) -> bool:
|
||||||
|
return self.future.cancel()
|
||||||
|
|
||||||
|
def done(self) -> bool:
|
||||||
|
return self.future.done()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shutdown(wait: bool = True):
|
||||||
|
BGTask.executor.shutdown(wait)
|
||||||
|
|
||||||
|
|
||||||
|
@register
|
||||||
|
def atexit():
|
||||||
|
BGTask.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_func(sql, *args, **kwargs):
|
||||||
|
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.execute(sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_many_func(sql, *args, **kwargs):
|
||||||
|
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.executemany(sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute(sql: str, *args, **kwargs):
|
||||||
|
'''异步执行SQL,日志库写入,注意不会直接返回结果'''
|
||||||
|
return BGTask(logdb_execute_func, sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_many(sql: str, *args, **kwargs):
|
||||||
|
'''异步批量执行SQL,日志库写入,注意不会直接返回结果'''
|
||||||
|
return BGTask(logdb_execute_many_func, sql, *args, **kwargs)
|
||||||
@@ -349,7 +349,7 @@ class UserCharacter(Character):
|
|||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
|
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
|
||||||
y = self.c.fetchone()
|
y = self.c.fetchone()
|
||||||
if not y or i.amount > y[0]:
|
if i.amount > 0 and (not y or i.amount > y[0]):
|
||||||
raise ItemNotEnough('The cores are not enough.')
|
raise ItemNotEnough('The cores are not enough.')
|
||||||
|
|
||||||
for i in self.uncap_cores:
|
for i in self.uncap_cores:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
|
|
||||||
ARCAEA_SERVER_VERSION = 'v2.11.1'
|
ARCAEA_SERVER_VERSION = 'v2.11.2'
|
||||||
|
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||||
|
|
||||||
|
|
||||||
class Constant:
|
class Constant:
|
||||||
@@ -101,4 +102,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', 'user_role']
|
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
|
||||||
|
|
||||||
|
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
|
||||||
|
|
||||||
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA
|
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ from time import time
|
|||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
from core.constant import ARCAEA_SERVER_VERSION
|
from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
|
||||||
from core.course import Course
|
from core.course import Course
|
||||||
from core.download import DownloadList
|
from core.download import DownloadList
|
||||||
from core.purchase import Purchase
|
from core.purchase import Purchase
|
||||||
from core.sql import Connect, DatabaseMigrator, MemoryDatabase
|
from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
|
||||||
|
MemoryDatabase)
|
||||||
from core.user import UserRegister
|
from core.user import UserRegister
|
||||||
from core.util import try_rename
|
from core.util import try_rename
|
||||||
|
|
||||||
@@ -208,6 +209,29 @@ class FileChecker:
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
# 检查更新
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
try:
|
||||||
|
x = c.execute(
|
||||||
|
'''select value from cache where key="version"''').fetchone()
|
||||||
|
except:
|
||||||
|
x = None
|
||||||
|
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
|
||||||
|
self.logger.warning(
|
||||||
|
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
|
||||||
|
try:
|
||||||
|
self.logger.info(
|
||||||
|
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
self.update_log_database()
|
||||||
|
self.logger.info(
|
||||||
|
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.error(
|
||||||
|
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
return False
|
||||||
|
|
||||||
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
||||||
# 新建数据库
|
# 新建数据库
|
||||||
try:
|
try:
|
||||||
@@ -275,6 +299,12 @@ class FileChecker:
|
|||||||
DatabaseMigrator(old_path, new_path).update_database()
|
DatabaseMigrator(old_path, new_path).update_database()
|
||||||
os.remove(old_path)
|
os.remove(old_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
|
||||||
|
'''直接更新日志数据库'''
|
||||||
|
if os.path.isfile(old_path):
|
||||||
|
LogDatabaseMigrator(old_path).update_database()
|
||||||
|
|
||||||
def check_song_file(self) -> bool:
|
def check_song_file(self) -> bool:
|
||||||
'''检查song有关文件并初始化缓存'''
|
'''检查song有关文件并初始化缓存'''
|
||||||
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from base64 import b64encode
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
from .bgtask import BGTask, logdb_execute
|
||||||
|
from .config_manager import Config
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .course import CoursePlay
|
from .course import CoursePlay
|
||||||
from .error import NoData, StaminaNotEnough
|
from .error import NoData, StaminaNotEnough
|
||||||
from .item import ItemCore
|
from .item import ItemCore
|
||||||
from .song import Chart
|
from .song import Chart
|
||||||
from .sql import Connect, Query, Sql
|
from .sql import Connect, Query, Sql
|
||||||
from .util import md5
|
from .util import get_today_timestamp, md5
|
||||||
from .world import WorldPlay
|
from .world import WorldPlay
|
||||||
|
|
||||||
|
|
||||||
@@ -427,10 +429,21 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
def record_score(self) -> None:
|
def record_score(self) -> None:
|
||||||
'''向log数据库记录分数,请注意列名不同'''
|
'''向log数据库记录分数,请注意列名不同'''
|
||||||
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2:
|
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
|
||||||
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))
|
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 record_rating_ptt(self, user_rating_ptt: float) -> None:
|
||||||
|
'''向log数据库记录用户ptt变化'''
|
||||||
|
today_timestamp = get_today_timestamp()
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
|
||||||
|
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
|
||||||
|
self.user.user_id, today_timestamp)).fetchone()
|
||||||
|
|
||||||
|
old_ptt = 0 if old_ptt is None else old_ptt[0]
|
||||||
|
if old_ptt != user_rating_ptt:
|
||||||
|
c2.execute('''insert or replace into user_rating values(?,?,?)''',
|
||||||
|
(self.user.user_id, today_timestamp, user_rating_ptt))
|
||||||
|
|
||||||
def upload_score(self) -> None:
|
def upload_score(self) -> None:
|
||||||
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
||||||
self.get_play_state()
|
self.get_play_state()
|
||||||
@@ -474,7 +487,9 @@ class UserPlay(UserScore):
|
|||||||
self.update_recent30()
|
self.update_recent30()
|
||||||
|
|
||||||
# 总PTT更新
|
# 总PTT更新
|
||||||
self.user.rating_ptt = int(self.ptt.value * 100)
|
user_rating_ptt = self.ptt.value
|
||||||
|
self.user.rating_ptt = int(user_rating_ptt * 100)
|
||||||
|
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
|
||||||
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
||||||
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import traceback
|
import traceback
|
||||||
from atexit import register
|
from atexit import register
|
||||||
|
|
||||||
from .constant import Constant
|
from .config_manager import Config
|
||||||
|
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
|
||||||
from .error import ArcError, InputError
|
from .error import ArcError, InputError
|
||||||
|
|
||||||
|
|
||||||
@@ -404,6 +406,31 @@ class DatabaseMigrator:
|
|||||||
self.update_user_char_full(c2) # 更新user_char_full
|
self.update_user_char_full(c2) # 更新user_char_full
|
||||||
|
|
||||||
|
|
||||||
|
class LogDatabaseMigrator:
|
||||||
|
|
||||||
|
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
|
||||||
|
self.c1_path = c1_path
|
||||||
|
# self.c2_path = c2_path
|
||||||
|
self.init_folder_path = Config.DATABASE_INIT_PATH
|
||||||
|
self.c = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sql_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'log_tables.sql')
|
||||||
|
|
||||||
|
def table_update(self) -> None:
|
||||||
|
'''直接更新数据库结构'''
|
||||||
|
with open(self.sql_path, 'r') as f:
|
||||||
|
self.c.executescript(f.read())
|
||||||
|
self.c.execute(
|
||||||
|
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
|
||||||
|
|
||||||
|
def update_database(self) -> None:
|
||||||
|
with Connect(self.c1_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_update()
|
||||||
|
|
||||||
|
|
||||||
class MemoryDatabase:
|
class MemoryDatabase:
|
||||||
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)
|
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)
|
||||||
|
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ 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, -203)
|
raise RateLimit(
|
||||||
|
f'Too many login attempts of username `{name}`', 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})
|
||||||
@@ -251,7 +252,7 @@ class UserLogin(User):
|
|||||||
# 自动封号检查
|
# 自动封号检查
|
||||||
ban_timestamp = int(x[2].split(':', 1)[1])
|
ban_timestamp = int(x[2].split(':', 1)[1])
|
||||||
if ban_timestamp > self.now:
|
if ban_timestamp > self.now:
|
||||||
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
|
raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
|
||||||
'remaining_ts': ban_timestamp-self.now})
|
'remaining_ts': ban_timestamp-self.now})
|
||||||
|
|
||||||
if x[1] == '':
|
if x[1] == '':
|
||||||
@@ -260,7 +261,7 @@ class UserLogin(User):
|
|||||||
f'The account `{self.user_id}` has been banned.', 106)
|
f'The account `{self.user_id}` has been banned.', 106)
|
||||||
|
|
||||||
if x[1] != self.hash_pwd:
|
if x[1] != self.hash_pwd:
|
||||||
raise NoAccess('Wrong password.', 104)
|
raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
|
||||||
|
|
||||||
self.token = base64.b64encode(hashlib.sha256(
|
self.token = base64.b64encode(hashlib.sha256(
|
||||||
(str(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()
|
||||||
@@ -763,7 +764,7 @@ class UserChanger(UserInfo, UserRegister):
|
|||||||
if columns is not None:
|
if columns is not None:
|
||||||
d = {}
|
d = {}
|
||||||
for column in columns:
|
for column in columns:
|
||||||
if column == 'password':
|
if column == 'password' and self.password != '':
|
||||||
d[column] = self.hash_pwd
|
d[column] = self.hash_pwd
|
||||||
else:
|
else:
|
||||||
d[column] = self.__dict__[column]
|
d[column] = self.__dict__[column]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
from datetime import date
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
|
||||||
def md5(code: str) -> str:
|
def md5(code: str) -> str:
|
||||||
@@ -37,3 +39,8 @@ def try_rename(path: str, new_path: str) -> str:
|
|||||||
|
|
||||||
os.rename(path, final_path)
|
os.rename(path, final_path)
|
||||||
return final_path
|
return final_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_today_timestamp():
|
||||||
|
'''相对于本机本地时间的今天0点的时间戳'''
|
||||||
|
return int(mktime(date.today().timetuple()))
|
||||||
|
|||||||
@@ -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', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata']
|
'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)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)']
|
||||||
|
|
||||||
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', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa']
|
'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_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
|
||||||
|
|
||||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', '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, 0, 0, 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, 0, 0, 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,
|
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, 30, 45, 57, 56, 47, 33, 26, 29, 66]
|
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, 45, 57, 56, 47, 33, 26, 29, 66, 40]
|
||||||
|
|
||||||
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, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35]
|
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, 45, 70, 37.5, 29, 44, 26, 26, 35, 40]
|
||||||
|
|
||||||
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, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5]
|
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, 45, 57, 31, 29, 65, 26, 29, 42.5, 40]
|
||||||
|
|
||||||
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, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100]
|
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, 75, 80, 90, 80, 50, 51, 54, 100, 50]
|
||||||
|
|
||||||
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, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53]
|
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, 75, 100, 60, 50, 68, 51, 50, 53, 85]
|
||||||
|
|
||||||
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, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5]
|
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, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 60]
|
||||||
|
|
||||||
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, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100]
|
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, 75, 80, 90, 80, 50, 51, 64, 100, 50]
|
||||||
|
|
||||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
|
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 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, 75, 100, 60, 50, 68, 51, 60, 53]
|
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, 75, 100, 60, 50, 68, 51, 60, 53, 85]
|
||||||
|
|
||||||
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, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5]
|
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, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 60]
|
||||||
|
|
||||||
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, 2, 0, 0, 2, 0, 0, 2, 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, 0, 2, 0, 0, 2, 0, 2, 2, 1]
|
||||||
|
|
||||||
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}],
|
||||||
@@ -59,14 +59,15 @@ class InitData:
|
|||||||
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||||
19: [{'core_id': 'core_colorful', 'amount': 30}],
|
19: [{'core_id': 'core_colorful', 'amount': 30}],
|
||||||
10: [{'core_id': 'core_umbral', 'amount': 30}],
|
10: [{'core_id': 'core_umbral', 'amount': 30}],
|
||||||
66: [{'core_id': 'core_chunithm', 'amount': 15}]
|
66: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
5: [{'core_id': 'core_hollow', 'amount': 0}]
|
||||||
}
|
}
|
||||||
|
|
||||||
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
|
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
|
||||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral']
|
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral']
|
||||||
|
|
||||||
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', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3']
|
"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', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris']
|
||||||
|
|
||||||
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"]
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ clear_type int,
|
|||||||
rating real,
|
rating real,
|
||||||
primary key(user_id, song_id, difficulty, time_played)
|
primary key(user_id, song_id, difficulty, time_played)
|
||||||
);
|
);
|
||||||
|
create table if not exists user_rating(user_id int,
|
||||||
|
time int,
|
||||||
|
rating_ptt real,
|
||||||
|
primary key(user_id, time)
|
||||||
|
);
|
||||||
|
|
||||||
create index if not exists user_score_1 on user_score (song_id, difficulty);
|
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);
|
create index if not exists user_score_2 on user_score (time_played);
|
||||||
|
|||||||
@@ -1378,5 +1378,95 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "thesurvivor",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "thesurvivor",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "newyorkbackraise",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "newyorkbackraise",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lostintheabyss",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "lostintheabyss",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "galacticlove",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "galacticlove",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tothemilkyway",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "tothemilkyway",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -32,7 +32,7 @@ def login():
|
|||||||
|
|
||||||
user = UserLogin(c)
|
user = UserLogin(c)
|
||||||
user.login(name, password, device_id, request.remote_addr)
|
user.login(name, password, device_id, request.remote_addr)
|
||||||
|
current_app.logger.info(f'User `{user.user_id}` log in')
|
||||||
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ def finale_start():
|
|||||||
return success_return({})
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/finale/finale_end', methods=['POST'])
|
||||||
|
def finale_end():
|
||||||
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
map_dict = {'/user/me': user_me,
|
map_dict = {'/user/me': user_me,
|
||||||
'/purchase/bundle/pack': bundle_pack,
|
'/purchase/bundle/pack': bundle_pack,
|
||||||
'/serve/download/me/song': download_song,
|
'/serve/download/me/song': download_song,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.character import UserCharacter
|
from core.character import UserCharacter
|
||||||
from core.error import ArcError
|
from core.error import ArcError
|
||||||
@@ -36,6 +36,7 @@ def register():
|
|||||||
user = UserLogin(c)
|
user = UserLogin(c)
|
||||||
user.login(new_user.name, new_user.password,
|
user.login(new_user.name, new_user.password,
|
||||||
device_id, request.remote_addr)
|
device_id, request.remote_addr)
|
||||||
|
current_app.logger.info(f'New user `{user.user_id}` registered')
|
||||||
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,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', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa']
|
'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', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
|
||||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
121
tools/clean_logdb.py
Normal file
121
tools/clean_logdb.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
LOG_DATABASE_PATH = './arcaea_log.db'
|
||||||
|
|
||||||
|
|
||||||
|
class Connect():
|
||||||
|
# 数据库连接类,上下文管理
|
||||||
|
|
||||||
|
def __init__(self, file_path=LOG_DATABASE_PATH):
|
||||||
|
"""
|
||||||
|
数据库连接
|
||||||
|
接受:文件路径
|
||||||
|
返回:sqlite3连接操作对象
|
||||||
|
"""
|
||||||
|
self.file_path = file_path
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.conn = sqlite3.connect(self.file_path)
|
||||||
|
self.c = self.conn.cursor()
|
||||||
|
return self.c
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||||
|
if exc_type is not None:
|
||||||
|
print(f'exc_type: {exc_type}')
|
||||||
|
print(f'exc_val: {exc_val}')
|
||||||
|
print(f'exc_tb: {exc_tb}')
|
||||||
|
if self.conn:
|
||||||
|
self.conn.rollback()
|
||||||
|
|
||||||
|
if self.conn:
|
||||||
|
self.conn.commit()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clean_by_time(table_name: str, time_colume_name: str, colume_count: int):
|
||||||
|
with Connect() as c:
|
||||||
|
today = datetime.now()
|
||||||
|
print(f'The time now is {today}.')
|
||||||
|
day = input(
|
||||||
|
'Please input the number of days the data before which you want to delete: ')
|
||||||
|
try:
|
||||||
|
day = int(day)
|
||||||
|
except ValueError:
|
||||||
|
print('Invalid input!')
|
||||||
|
return
|
||||||
|
time = mktime(today.timetuple()) - day * 24 * 60 * 60
|
||||||
|
delete_count = c.execute(
|
||||||
|
f'select count(*) from {table_name} where {time_colume_name} < ?', (time,)).fetchone()[0]
|
||||||
|
all_count = c.execute(
|
||||||
|
f'select count(*) from {table_name}').fetchone()[0]
|
||||||
|
print(
|
||||||
|
f'Before {day} days, there are {delete_count} records to be deleted, {all_count} records in total.')
|
||||||
|
flag = input('Are you sure to delete these records? (y/n) ')
|
||||||
|
if flag == 'y' or flag == 'Y':
|
||||||
|
if delete_count >= 1000000:
|
||||||
|
print(
|
||||||
|
'It will cost a long time to delete these records, please wait patiently...')
|
||||||
|
print('Deleting...')
|
||||||
|
c.execute('PRAGMA cache_size = 32768')
|
||||||
|
c.execute('PRAGMA synchronous = OFF')
|
||||||
|
c.execute('PRAGMA temp_store = MEMORY')
|
||||||
|
if delete_count / all_count >= 0.8:
|
||||||
|
data = c.execute(
|
||||||
|
f'select * from {table_name} where {time_colume_name} > ?', (time,)).fetchall()
|
||||||
|
c.execute(f'delete from {table_name}')
|
||||||
|
c.executemany(
|
||||||
|
f'insert into {table_name} values ({",".join(["?"]*colume_count)})', data)
|
||||||
|
else:
|
||||||
|
c.execute(
|
||||||
|
f'delete from {table_name} where {time_colume_name} < ?', (time,))
|
||||||
|
c.execute('PRAGMA temp_store = DEFAULT')
|
||||||
|
print('Delete successfully!')
|
||||||
|
else:
|
||||||
|
print('Delete canceled!')
|
||||||
|
|
||||||
|
|
||||||
|
def vacuum():
|
||||||
|
print('This operation will release unused space in the database file.')
|
||||||
|
print('It will cost a long time to release unused space if the database file is so large.')
|
||||||
|
flag = input('Are you sure to release unused space? (y/n) ')
|
||||||
|
if flag == 'y' or flag == 'Y':
|
||||||
|
with Connect() as c:
|
||||||
|
print('Releasing unused space...')
|
||||||
|
c.execute('vacuum')
|
||||||
|
print('Release unused space successfully!')
|
||||||
|
else:
|
||||||
|
print('Release unused space canceled!')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(LOG_DATABASE_PATH):
|
||||||
|
print('The database file `arcaea_log.db` does not exist!')
|
||||||
|
print('-- Arcaea Server Log Database Cleaner --')
|
||||||
|
print('Note: It is more recommended to delete the database file directly.')
|
||||||
|
while True:
|
||||||
|
print('-' * 40)
|
||||||
|
print('1. clean `user_score` table')
|
||||||
|
print('2. clean `user_rating` table')
|
||||||
|
print('3. release unused space (`vacuum` command)')
|
||||||
|
print('0. exit')
|
||||||
|
choice = input('Please input your choice: ')
|
||||||
|
if choice == '1':
|
||||||
|
clean_by_time('user_score', 'time_played', 13)
|
||||||
|
elif choice == '2':
|
||||||
|
clean_by_time('user_rating', 'time', 3)
|
||||||
|
elif choice == '3':
|
||||||
|
vacuum()
|
||||||
|
elif choice == '0':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print('Invalid choice!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
input('Press `Enter` key to exit.')
|
||||||
Reference in New Issue
Block a user