├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── command
├── __init__.py
├── mai_alias.py
├── mai_base.py
├── mai_guess.py
├── mai_score.py
├── mai_search.py
└── mai_table.py
├── libraries
├── image.py
├── maimai_best_50.py
├── maimaidx_api_data.py
├── maimaidx_arcade.py
├── maimaidx_error.py
├── maimaidx_model.py
├── maimaidx_music.py
├── maimaidx_music_info.py
├── maimaidx_player_score.py
├── maimaidx_update_table.py
└── tool.py
├── maimai.py
├── maimai_arcade.py
├── maimaidxhelp.png
├── requirements.txt
└── static
├── config.json
├── echarts.min.js
└── temp_pie.html
/.gitignore:
--------------------------------------------------------------------------------
1 | # static
2 | static/
3 | arcades.json
4 |
5 | __pycache__
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Yuri-YuzuChaN
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # maimaiDX
2 |
3 | [](https://www.python.org/)
4 | [](https://qm.qq.com/q/gDIf3fGSPe)
5 | [](https://opensource.org/licenses/MIT)
6 |
7 | 移植自[mai-bot](https://github.com/Diving-Fish/mai-bot) 开源项目,基于 [HoshinoBotV2](https://github.com/Ice-Cirno/HoshinoBot) 的街机音游 **舞萌DX** 的查询插件
8 |
9 | 项目地址:https://github.com/Yuri-YuzuChaN/maimaiDX
10 |
11 | ## 重要更新
12 |
13 | **2025-03-28**
14 |
15 | > [!WARNING]
16 | > 对于这个版本之前的插件和修改版的插件请注意,预更新版本别名库将全部更换成新的 `API` 地址,返回的数据结构均更改,目前旧版 `API` 将再运行一段时间,预计正式更新 `舞萌DX2025` 时将会关闭
17 |
18 | 1. 预更新 `舞萌DX2025` UI,资源全部更换,更新部分依赖和文件,**请重新进行使用方法**
19 | 2. 修改所有 `BOT管理员` 私聊指令为群聊指令:`更新别名库`、`更新maimai数据`、`更新定数表`、`更新完成表`
20 |
21 | ## 使用方法
22 |
23 | 1. 将该项目放在HoshinoBot插件目录 `modules` 下,或者clone本项目
24 |
25 | ``` git
26 | git clone https://github.com/Yuri-YuzuChaN/maimaiDX
27 | ```
28 |
29 | 2. 下载静态资源文件,将该压缩文件解压后,将 `static` 文件夹复制到插件根目录并覆盖,即 `maimaiDX/static`
30 |
31 | - [私人云盘](https://cloud.yuzuchan.moe/f/1bUn/Resource.7z)
32 | - [AList网盘](https://share.yuzuchan.moe/p/Resource.7z?sign=EvCwaGwJrneyD1Olq00NG3HXNK7fQKpx_sa3Ck9Uzjs=:0)
33 | - [onedrive](https://yuzuai-my.sharepoint.com/:u:/g/personal/yuzu_yuzuchan_moe/EdGUKRSo-VpHjT2noa_9EroBdFZci-tqWjVZzKZRTEeZkw?e=a1TM40)
34 |
35 | 3. 配置可选项,请修改 `maimaiDX/static/config.json` 文件
36 |
37 | 1. 如果您拥有查分器的开发者 `token`,请将 `token` 填入文件中的 `maimaidxtoken` 项
38 | 2. 如果你的服务器或主机不能顺利流畅的访问查分器和别名库的API,请配置代理。均为香港服务器代理中转,例如你的服务器访问查分器很困难,请设置 `maimaidxproberproxy` 为 `true`,别名库同理
39 | 3. 可选,是否将部分图片在保存在内存中,不需要请在设置 `SAVEINMEM` 为 `false`
40 | ``` json
41 | {
42 | "maimaidxtoken": "maimaidxtoken",
43 | "maimaidxproberproxy": true,
44 | "maimaidxaliasproxy": false,
45 | "saveinmem": false
46 | }
47 | ```
48 |
49 | 4. 安装插件所需模块:`pip install -r requirements.txt`
50 | 5. 安装 `chromium`,**相关依赖已安装,请直接使用该指令执行**
51 |
52 | ``` shell
53 | playwright install --with-deps chromium
54 | ```
55 |
56 | 6. 安装 `微软雅黑` 字体,解决使用 `ginfo` 指令字体不渲染的问题,例如 `ubuntu`:`apt install fonts-wqy-microhei`,`windows` 平台可跳过
57 | 7. 在 `config/__bot__.py` 模块列表中添加 `maimaiDX`
58 | 8. 重启HoshinoBot
59 |
60 | ## 更新说明
61 |
62 | **2025-03-28**
63 |
64 | 1. 预更新 `舞萌DX2025` UI
65 | 2. 修改所有 `BOT管理员` 私聊指令为群聊指令:`更新别名库`、`更新maimai数据`、`更新定数表`、`更新完成表`
66 |
67 | **2024-07-24**
68 |
69 | 1. 更新部分牌子完成表和 `SyncPlay` 图片
70 | 2. 修复 `新增机厅` 指令 `id` 未增加的问题
71 | 3. 修复 `牌子进度` 指令 `sync` 未匹配的问题
72 | 4. 修复 `别名查歌` 指令查询到已删除的曲目时发生错误的问题
73 |
74 | **2024-06-07**
75 |
76 | 1. 更新至 `舞萌DX 2024`
77 | 2. 更换所有图片绘制,需删除除 `json` 后缀的所有文件,**请重新进行使用方法第二步**
78 | 3. 更改部分 `json` 文件名称,便于识别,具体文件如下,**请务必修改文件名,否则开关文件以及本地别名文件将不会被读取**
79 | - `all_alias.json` 修改为 `music_alias.json`
80 | - `local_alias.json` 修改为 `local_music_alias.json`
81 | - `chart_stats.json` 修改为 `music_chart.json`
82 | - `group_alias.json` 修改为 `group_alias_switch.json`
83 | - `guess_config.json` 修改为 `group_guess_switch.json`
84 | 4. 新增管理员私聊指令 `更新完成表`,用于更新 `BUDDiES` 版本 `双系` 牌子
85 | 5. 新增指令 `完成表`,可查询牌子完成表,例如:`祝极完成表`
86 | 6. 新增指令 `猜曲绘`
87 | 7. 查看谱面支持计算个人加分情况,指令包括 `是什么歌`,`id`
88 | 8. 指令 `mai什么` 支持随机发送推分谱面,指令中需包含 `加分`,`上分` 字样,例如:`今日mai打什么上分`
89 | 9. 修改指令 `分数列表` 和 `进度` 发送方式
90 | 10. 优化所有模块
91 |
92 | **2024-03-12**
93 |
94 | 1. 变更别名服务器地址
95 | 2. 修改所有别名请求以及参数
96 | 3. 开放普通用户申请别名
97 |
98 | **2024-01-14**
99 |
100 | 1. 优先使用本地谱面
101 | 2. 使用 `numpy` 模块重新绘制定数表
102 |
103 | **2023-09-23**
104 |
105 | 1. 重写 `API` 方法
106 | 2. 重写机厅模块
107 | 3. 将同步生成定数表方法修改为异步方法,防止堵塞进程
108 | 4. 将 `当前别名投票` 发送方式修改为图片形式
109 | 5. 本地添加别名单独存储为一个文件,不再添加在暂存别名文件中
110 |
111 | **2023-08-10**
112 |
113 | 1. 新增后缀指令 `定数表`,`完成表`,查询指定等级的定数表和完成表,例如:`13+完成表`
114 | 2. 新增BOT管理员私聊指令 `更新定数表`,用于生成和更新定数表
115 | 3. 新增BOT管理员私聊指令 `更新maimai数据`,用于版本更新手动更新bot已存数据
116 | 4. 拆分并移除 `maimaidx_project.py` 的代码和文件,便于所有功能维护
117 | 5. 修复曲绘不存在时下载错误的问题
118 | 6. 修复猜歌提前发出答案的bug
119 | 7. 修改指令 `minfo` 部分绘图
120 |
121 | **2023-06-15**
122 |
123 | 1. 新增添加本地别名的功能
124 |
125 | **2023-06-09**
126 |
127 | 1. 更新至 `舞萌DX 2023`
128 | 2. 移除指令 `b40`
129 | 3. 更换静态资源
130 | 4. 修改指令 `b50` 部分绘图
131 |
132 | **2023-04-22**
133 |
134 | 1. 限制所有网络请求时长
135 | 2. 新增别名文件本地备份
136 | 3. 新增ginfo指令默认使用紫谱数据
137 |
138 | **2023-04-21**
139 |
140 | 1. 新增BOT管理员私聊指令 `全局关闭别名推送` 和 `全局开启别名推送`,关闭所有群的推送消息,无论先前开启还是关闭
141 | 2. 修复新版本更新后API暂未收录曲目的问题
142 | 3. 新增乐曲游玩总览 `ginfo` 指令
143 | 4. 新增猜歌库根据乐曲游玩次数添加
144 | 5. 新增每日更新机厅信息,删除旧版更新机厅机制
145 |
146 | **2023-04-15**
147 |
148 | 1. 将获取数据的方式由启动Bot时获取改为连接到CQHTTP后获取
149 | 2. 修复因查分器API内容变动而无法启动Bot的问题
150 |
151 | **2023-03-29**
152 |
153 | 1. 重制 `b40/b50` ,`minfo` 和曲目信息的绘图
154 | 2. 修改投票网页端,改成共用网站
155 | 3. 修改垃圾代码
156 |
157 | **2023-03-02**
158 |
159 | 1. 新增 `开启别名推送` 和 `关闭别名推送` 指令
160 |
161 | **2023-02-25**
162 |
163 | 1. 修复猜歌答对后无法结束的问题
164 |
165 | **2023-02-23**
166 |
167 | 1. 投票网页端
168 |
169 | **2023-02-22**
170 |
171 | 1. 修复启动BOT时无法获取所有曲目信息的问题,添加本地缓存
172 | 2. 修改别名库,使用API获取和添加,并同步所有使用该插件的BOT
173 | 3. 修改猜歌和别名功能
174 | 4. 新增指令 `当前别名投票` 和 `同意别名`
175 |
176 | **2023-2-18**
177 |
178 | 1. 别称同步临时解决方案 #47
179 |
180 | **2023-2-15**
181 |
182 | 1. 更新本地缓存水鱼网数据 #43
183 |
184 | **2022-9-14**
185 |
186 | 1. 新增查询单曲指令 `minfo`
187 | 2. 修改查曲绘图
188 |
189 | **2022-8-30**
190 |
191 | 1. 修复新版b40/b50 isinstance bug [#38](https://github.com/Yuri-YuzuChaN/maimaiDX/issues/38)
192 | 2. 修复新版b40/b50 找不到图片问题
193 | 3. 修复安慰分隐性bug
194 |
195 | **2022-08-27**
196 |
197 | 1. 修复b40/b50小数点后四位错误的问题
198 |
199 | **2022-08-25**
200 |
201 | 1. 修复猜歌模块发送曲绘时为未知曲绘的问题
202 |
203 | **2022-08-16**
204 |
205 | 1. 修改 `b40/b50` 指令绘图,如不喜欢请将 `libraries/maimaidx_project.py` 第`6`行 `maimai_best_50` 改成 `maimai_best_40`
206 | 2. 修改查曲绘图
207 |
208 | **2022-07-11**
209 |
210 | 1. 修复指令 `分数列表` 没有提供2022谱面的问题
211 |
212 | **2022-06-23**
213 |
214 | 1. 支持2022
215 | 2. 修改所有曲绘后缀
216 | 3. 修改获取在线文件的路径
217 |
218 | **2022-03-10**
219 |
220 | 1. 新增段位显示,感谢 [Kurokitu](https://github.com/Kurokitu) 提供源码及资源
221 |
222 | **2022-02-13**
223 |
224 | 1. 修复部分新曲没有难易度参考的问题
225 |
226 | **2022-01-27**
227 |
228 | 1. 修复添加/删除别名无效的问题
229 |
230 | **2022-01-16**
231 |
232 | 1. 修复b40/b50查询@Ta人情况下无效的问题
233 |
234 | **2022-01-03**
235 |
236 | 1. 修改获取音乐数据的函数,不在使用同步进程
237 | 2. 不再使用正则表达式获取@人员的QQ号
238 | 3. 不再使用CQ码方式发送图片
239 | 4. 修改大部分源码
240 |
241 | **2021-11-15**
242 |
243 | 1. 在请求获取maimaiDX数据的函数添加 `@retry` 装饰器,遇到请求数据失败的情况时重新尝试请求
244 |
245 | **2021-10-18**
246 |
247 | 1. 添加排卡功能,感谢 [CrazyKid](https://github.com/CrazyKidCN)
248 |
249 | **2021-10-14**
250 |
251 | 1. 更新查看推荐的上分乐曲
252 | 2. 更新查看牌子完成进度
253 | 3. 更新查看等级评价完成进度
254 | 4. 查看水鱼网站的用户ra排行
255 |
256 | **2021-09-29**
257 |
258 | 1. 更新b50、乐曲推荐功能,感谢 [BlueDeer233](https://github.com/BlueDeer233)
259 |
260 | **2021-09-13**
261 |
262 | 1. 更新猜歌功能以及开关,感谢 [BlueDeer233](https://github.com/BlueDeer233)
263 |
264 |
265 | ## 鸣谢
266 |
267 | 感谢 [zhanbao2000](https://github.com/zhanbao2000) 提供的 `nonebot2` 分支
268 |
269 | 感谢 [CrazyKid](https://github.com/CrazyKidCN) 提供的源码支持
270 |
271 | 感谢 [Diving-Fish](https://github.com/Diving-Fish) 提供的源码支持
272 |
273 | 感谢 [BlueDeer233](https://github.com/BlueDeer233) 提供猜歌功能的源码支持
274 |
275 | ## License
276 |
277 | MIT
278 |
279 | 您可以自由使用本项目的代码用于商业或非商业的用途,但必须附带 MIT 授权协议。
280 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Dict, List
3 |
4 | from hoshino import Service, priv
5 | from hoshino.config import NICKNAME
6 | from hoshino.log import new_logger
7 |
8 | ### 必须
9 | log = new_logger('maimaiDX')
10 | loga = new_logger('maimaiDXArcade')
11 | BOTNAME = NICKNAME if isinstance(NICKNAME, str) else list(NICKNAME)[0]
12 |
13 |
14 | SV_HELP = '请使用 帮助maimaiDX 查看帮助'
15 | sv = Service('maimaiDX', manage_priv=priv.ADMIN, enable_on_default=True, help_=SV_HELP)
16 |
17 | public_addr = 'https://www.yuzuchan.moe/vote'
18 |
19 |
20 | # echartsjs
21 | SNAPSHOT_JS = (
22 | "echarts.getInstanceByDom(document.querySelector('div[_echarts_instance_]'))."
23 | "getDataURL({type: 'PNG', pixelRatio: 2, excludeComponents: ['toolbox']})"
24 | )
25 |
26 |
27 | # 文件路径
28 | Root: Path = Path(__file__).parent
29 | static: Path = Root / 'static'
30 |
31 | arcades_json: Path = static / 'arcades.json' # 机厅
32 | config_json: Path = static / 'config.json' # token
33 | alias_file: Path = static / 'music_alias.json' # 别名暂存文件
34 | local_alias_file: Path = static / 'local_music_alias.json' # 本地别名文件
35 | music_file: Path = static / 'music_data.json' # 曲目暂存文件
36 | chart_file: Path = static / 'music_chart.json' # 谱面数据暂存文件
37 | guess_file: Path = static / 'group_guess_switch.json' # 猜歌开关群文件
38 | group_alias_file: Path = static / 'group_alias_switch.json' # 别名推送开关群文件
39 | pie_html_file: Path = static / 'temp_pie.html' # 饼图html文件
40 |
41 |
42 | # 静态资源路径
43 | maimaidir: Path = static / 'mai' / 'pic'
44 | coverdir: Path = static / 'mai' / 'cover'
45 | ratingdir: Path = static / 'mai' / 'rating'
46 | platedir: Path = static / 'mai' / 'plate'
47 |
48 |
49 | # 字体路径
50 | SIYUAN: Path = static / 'ResourceHanRoundedCN-Bold.ttf'
51 | SHANGGUMONO: Path = static / 'ShangguMonoSC-Regular.otf'
52 | TBFONT: Path = static / 'Torus SemiBold.otf'
53 |
54 |
55 | # 常用变量
56 | SONGS_PER_PAGE: int = 25
57 | scoreRank: List[str] = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 's+', 'ss', 'ss+', 'sss', 'sss+']
58 | score_Rank: List[str] = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp']
59 | score_Rank_l: Dict[str, str] = {
60 | 'd': 'D',
61 | 'c': 'C',
62 | 'b': 'B',
63 | 'bb': 'BB',
64 | 'bbb': 'BBB',
65 | 'a': 'A',
66 | 'aa': 'AA',
67 | 'aaa': 'AAA',
68 | 's': 'S',
69 | 'sp': 'Sp',
70 | 'ss': 'SS',
71 | 'ssp': 'SSp',
72 | 'sss': 'SSS',
73 | 'sssp': 'SSSp'
74 | }
75 | comboRank: List[str] = ['fc', 'fc+', 'ap', 'ap+']
76 | combo_rank: List[str] = ['fc', 'fcp', 'ap', 'app']
77 | syncRank: List[str] = ['fs', 'fs+', 'fdx', 'fdx+']
78 | sync_rank: List[str] = ['fs', 'fsp', 'fsd', 'fsdp']
79 | sync_rank_p: List[str] = ['fs', 'fsp', 'fdx', 'fdxp']
80 | diffs: List[str] = ['Basic', 'Advanced', 'Expert', 'Master', 'Re:Master']
81 | levelList: List[str] = ['1', '2', '3', '4', '5', '6', '7', '7+', '8', '8+', '9', '9+', '10', '10+', '11', '11+', '12', '12+', '13', '13+', '14', '14+', '15']
82 | achievementList: List[float] = [50.0, 60.0, 70.0, 75.0, 80.0, 90.0, 94.0, 97.0, 98.0, 99.0, 99.5, 100.0, 100.5]
83 | BaseRaSpp: List[float] = [7.0, 8.0, 9.6, 11.2, 12.0, 13.6, 15.2, 16.8, 20.0, 20.3, 20.8, 21.1, 21.6, 22.4]
84 | fcl: Dict[str, str] = {'fc': 'FC', 'fcp': 'FCp', 'ap': 'AP', 'app': 'APp'}
85 | fsl: Dict[str, str] = {'fs': 'FS', 'fsp': 'FSp', 'fsd': 'FSD', 'fdx': 'FSD', 'fsdp': 'FSDp', 'fdxp': 'FSDp', 'sync': 'Sync'}
86 | plate_to_version: Dict[str, str] = {
87 | '初': 'maimai',
88 | '真': 'maimai PLUS',
89 | '超': 'maimai GreeN',
90 | '檄': 'maimai GreeN PLUS',
91 | '橙': 'maimai ORANGE',
92 | '暁': 'maimai ORANGE PLUS',
93 | '晓': 'maimai ORANGE PLUS',
94 | '桃': 'maimai PiNK',
95 | '櫻': 'maimai PiNK PLUS',
96 | '樱': 'maimai PiNK PLUS',
97 | '紫': 'maimai MURASAKi',
98 | '菫': 'maimai MURASAKi PLUS',
99 | '堇': 'maimai MURASAKi PLUS',
100 | '白': 'maimai MiLK',
101 | '雪': 'MiLK PLUS',
102 | '輝': 'maimai FiNALE',
103 | '辉': 'maimai FiNALE',
104 | '熊': 'maimai でらっくす',
105 | '華': 'maimai でらっくす PLUS',
106 | '华': 'maimai でらっくす PLUS',
107 | '爽': 'maimai でらっくす Splash',
108 | '煌': 'maimai でらっくす Splash PLUS',
109 | '宙': 'maimai でらっくす UNiVERSE',
110 | '星': 'maimai でらっくす UNiVERSE PLUS',
111 | '祭': 'maimai でらっくす FESTiVAL',
112 | '祝': 'maimai でらっくす FESTiVAL PLUS',
113 | '双': 'maimai でらっくす BUDDiES',
114 | '宴': 'maimai でらっくす BUDDiES PLUS'
115 | }
116 | platecn = {
117 | '晓': '暁',
118 | '樱': '櫻',
119 | '堇': '菫',
120 | '辉': '輝',
121 | '华': '華'
122 | }
123 | category: Dict[str, str] = {
124 | '流行&动漫': 'anime',
125 | '舞萌': 'maimai',
126 | 'niconico & VOCALOID': 'niconico',
127 | '东方Project': 'touhou',
128 | '其他游戏': 'game',
129 | '音击&中二节奏': 'ongeki',
130 | 'POPSアニメ': 'anime',
131 | 'maimai': 'maimai',
132 | 'niconicoボーカロイド': 'niconico',
133 | '東方Project': 'touhou',
134 | 'ゲームバラエティ': 'game',
135 | 'オンゲキCHUNITHM': 'ongeki',
136 | '宴会場': '宴会场'
137 | }
--------------------------------------------------------------------------------
/command/__init__.py:
--------------------------------------------------------------------------------
1 | from .mai_alias import *
2 | from .mai_base import *
3 | from .mai_guess import *
4 | from .mai_score import *
5 | from .mai_search import *
6 | from .mai_table import *
7 |
--------------------------------------------------------------------------------
/command/mai_alias.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import re
3 | import traceback
4 | from re import Match
5 | from textwrap import dedent
6 | from typing import List
7 |
8 | from nonebot import NoneBot
9 |
10 | from hoshino.service import priv
11 | from hoshino.typing import CQEvent, MessageSegment
12 |
13 | from .. import SONGS_PER_PAGE, log, public_addr, sv
14 | from ..libraries.image import image_to_base64, text_to_image
15 | from ..libraries.maimaidx_api_data import maiApi
16 | from ..libraries.maimaidx_error import AliasesNotFoundError, ServerError
17 | from ..libraries.maimaidx_music import alias, mai, update_local_alias
18 | from ..libraries.maimaidx_music_info import draw_music_info
19 |
20 | update_alias = sv.on_fullmatch('更新别名库')
21 | alias_switch_on = sv.on_fullmatch('全局开启别名推送')
22 | alias_switch_off = sv.on_fullmatch('全局关闭别名推送')
23 | alias_local_apply = sv.on_prefix(['添加本地别名', '添加本地别称'])
24 | alias_apply = sv.on_prefix(['添加别名', '增加别名', '增添别名', '添加别称'])
25 | alias_agree = sv.on_prefix(['同意别名', '同意别称'])
26 | alias_status = sv.on_prefix(['当前投票', '当前别名投票', '当前别称投票'])
27 | alias_switch = sv.on_suffix(['别名推送', '别称推送'])
28 | alias_song = sv.on_rex(re.compile(r'^(id)?\s?(.+)\s?有什么别[名称]$', re.IGNORECASE))
29 | alias_apply_status = sv.scheduled_job('interval', minutes=5)
30 |
31 |
32 | @update_alias
33 | async def _(bot: NoneBot, ev: CQEvent):
34 | if not priv.check_priv(ev, priv.SUPERUSER):
35 | return
36 | try:
37 | await mai.get_music_alias()
38 | log.info('手动更新别名库成功')
39 | await bot.send(ev, '手动更新别名库成功')
40 | except:
41 | log.error('手动更新别名库失败')
42 | await bot.send(ev, '手动更新别名库失败')
43 |
44 |
45 | @alias_switch_on
46 | @alias_switch_off
47 | async def _(bot: NoneBot, ev: CQEvent):
48 | if not priv.check_priv(ev, priv.SUPERUSER):
49 | return
50 | if ev.raw_message == '全局关闭别名推送':
51 | await alias.alias_global_change(False)
52 | await bot.send(ev, '已全局关闭maimai别名推送')
53 | elif ev.raw_message == '全局开启别名推送':
54 | await alias.alias_global_change(True)
55 | await bot.send(ev, '已全局开启maimai别名推送')
56 |
57 |
58 | @alias_local_apply
59 | async def _(bot: NoneBot, ev: CQEvent):
60 | args: List[str] = ev.message.extract_plain_text().strip().split()
61 | if len(args) != 2:
62 | await alias_local_apply.finish(ev, '参数错误', at_sender=True)
63 | song_id, alias_name = args
64 | if not mai.total_list.by_id(song_id):
65 | await bot.finish(ev, f'未找到ID为「{song_id}」的曲目', at_sender=True)
66 | try:
67 | server_exist = await maiApi.get_songs_alias(song_id)
68 | if alias_name.lower() in server_exist.Alias:
69 | await bot.send(ev, f'该曲目的别名「{alias_name}」已存在别名服务器,不能重复添加别名', at_sender=True)
70 | await mai.get_music_alias()
71 | await bot.finish(ev, f'别名库更新完成', at_sender=True)
72 | except AliasesNotFoundError:
73 | pass
74 |
75 | local_exist = mai.total_alias_list.by_id(song_id)
76 | if local_exist and alias_name.lower() in local_exist[0].Alias:
77 | await bot.finish(ev, f'本地别名库已存在该别名', at_sender=True)
78 |
79 | issave = await update_local_alias(song_id, alias_name)
80 | if not issave:
81 | msg = '添加本地别名失败'
82 | else:
83 | msg = f'已成功为ID「{song_id}」添加别名「{alias_name}」到本地别名库'
84 | await bot.send(ev, msg, at_sender=True)
85 |
86 |
87 | @alias_apply
88 | async def _(bot: NoneBot, ev: CQEvent):
89 | try:
90 | args: List[str] = ev.message.extract_plain_text().strip().split()
91 | if len(args) != 2:
92 | await bot.finish(ev, '参数错误', at_sender=True)
93 | song_id, alias_name = args
94 | if not (music := mai.total_list.by_id(song_id)):
95 | await bot.finish(ev, f'未找到ID为「{song_id}」的曲目')
96 | try:
97 | isexist = await maiApi.get_songs_alias(song_id)
98 | if alias_name.lower() in isexist.Alias:
99 | await bot.send(
100 | ev,
101 | f'该曲目的别名「{alias_name}」已存在别名服务器,不能重复添加别名,正在进行更新别名库',
102 | at_sender=True
103 | )
104 | await mai.get_music_alias()
105 | await bot.finish(ev, f'别名库更新完成', at_sender=True)
106 | except AliasesNotFoundError:
107 | pass
108 |
109 | status = await maiApi.post_alias(song_id, alias_name, ev.user_id)
110 | msg = dedent(f'''\
111 | 您已提交以下别名申请
112 | ID:{song_id}
113 | 别名:{alias_name}
114 | 现在可用使用唯一标签「{status.Tag}」来进行投票,例如:同意别名 {status.Tag}
115 | 浏览 {public_addr} 查看详情
116 | {await draw_music_info(music)}
117 | ''')
118 | except (ServerError, ValueError) as e:
119 | log.error(traceback.format_exc())
120 | msg = str(e)
121 | await bot.send(ev, msg, at_sender=True)
122 |
123 |
124 | @alias_agree
125 | async def _(bot: NoneBot, ev: CQEvent):
126 | try:
127 | tag: str = ev.message.extract_plain_text().strip().upper()
128 | status = await maiApi.post_agree_user(tag, ev.user_id)
129 | await bot.send(ev, status, at_sender=True)
130 | except ValueError as e:
131 | await bot.send(ev, str(e), at_sender=True)
132 |
133 |
134 | @alias_status
135 | async def _(bot: NoneBot, ev: CQEvent):
136 | try:
137 | args: str = ev.message.extract_plain_text().strip()
138 | status = await maiApi.get_alias_status()
139 | if not status:
140 | await bot.finish(ev, '未查询到正在进行的别名投票', at_sender=True)
141 |
142 | page = max(min(int(args), len(status) // SONGS_PER_PAGE + 1), 1) if args else 1
143 | result = []
144 | for num, _s in enumerate(status):
145 | if (page - 1) * SONGS_PER_PAGE <= num < page * SONGS_PER_PAGE:
146 | apply_alias = _s.ApplyAlias
147 | if len(_s.ApplyAlias) > 15:
148 | apply_alias = _s.ApplyAlias[:15] + '...'
149 | r = f'{_s.Tag}:\n- ID:{_s.SongID}\n- 别名:{apply_alias}\n- 票数:{_s.AgreeVotes}/{_s.Votes}'
150 | result.append(r)
151 | result.append(f'第{page}页,共{len(status) // SONGS_PER_PAGE + 1}页')
152 | msg = MessageSegment.image(image_to_base64(text_to_image('\n'.join(result))))
153 | except (ServerError, ValueError) as e:
154 | log.error(traceback.format_exc())
155 | msg = str(e)
156 | await bot.send(ev, msg, at_sender=True)
157 |
158 |
159 | @alias_song
160 | async def _(bot: NoneBot, ev: CQEvent):
161 | match: Match[str] = ev['match']
162 | findid = bool(match.group(1))
163 | name = match.group(2)
164 | aliases = None
165 | if findid and name.isdigit():
166 | alias_id = mai.total_alias_list.by_id(name)
167 | if not alias_id:
168 | await bot.finish(ev, '未找到此歌曲\n可以使用「添加别名」指令给该乐曲添加别名', at_sender=True)
169 | else:
170 | aliases = alias_id
171 | else:
172 | aliases = mai.total_alias_list.by_alias(name)
173 | if not aliases:
174 | if name.isdigit():
175 | alias_id = mai.total_alias_list.by_id(name)
176 | if not alias_id:
177 | await bot.finish(ev, '未找到此歌曲\n可以使用「添加别名」指令给该乐曲添加别名', at_sender=True)
178 | else:
179 | aliases = alias_id
180 | else:
181 | await bot.finish(ev, '未找到此歌曲\n可以使用「添加别名」指令给该乐曲添加别名', at_sender=True)
182 | if len(aliases) != 1:
183 | msg = []
184 | for songs in aliases:
185 | alias_list = '\n'.join(songs.Alias)
186 | msg.append(f'ID:{songs.SongID}\n{alias_list}')
187 | await bot.finish(ev, f'找到{len(aliases)}个相同别名的曲目:\n' + '\n======\n'.join(msg), at_sender=True)
188 |
189 | if len(aliases[0].Alias) == 1:
190 | await bot.finish(ev, '该曲目没有别名', at_sender=True)
191 |
192 | msg = f'该曲目有以下别名:\nID:{aliases[0].SongID}\n'
193 | msg += '\n'.join(aliases[0].Alias)
194 | await bot.send(ev, msg, at_sender=True)
195 |
196 |
197 | @alias_switch
198 | async def _(bot: NoneBot, ev: CQEvent):
199 | args = ev.message.extract_plain_text().strip().lower()
200 | if args == '开启':
201 | msg = await alias.on(ev.group_id)
202 | elif args == '关闭':
203 | msg = await alias.off(ev.group_id)
204 | else:
205 | raise ValueError('matcher type error')
206 |
207 | await bot.send(ev, msg)
208 |
209 | @alias_apply_status
210 | async def _():
211 | try:
212 | group = await sv.get_enable_groups()
213 | status = await maiApi.get_alias_status()
214 | if not alias.push.global_switch:
215 | await mai.get_music_alias()
216 | return
217 | if status:
218 | msg = ['检测到新的别名申请']
219 | msg2 = ['以下是已成功添加别名的曲目']
220 | for _s in status:
221 | if _s.IsNew and (usernum := _s.AgreeVotes) < (votes := _s.Votes):
222 | song_id = str(_s.SongID)
223 | alias_name = _s.ApplyAlias
224 | music = mai.total_list.by_id(song_id)
225 | msg.append(f'{_s.Tag}:\nID:{song_id}\n标题:{music.title}\n别名:{alias_name}\n票数:{usernum}/{votes}')
226 | elif _s.IsEnd:
227 | song_id = str(_s.SongID)
228 | alias_name = _s.ApplyAlias
229 | music = mai.total_list.by_id(song_id)
230 | msg2.append(f'ID:{song_id}\n标题:{music.title}\n别名:{alias_name}')
231 |
232 | if len(msg) != 1 and len(msg2) != 1:
233 | for gid in group.keys():
234 | if gid in alias.push.disable:
235 | continue
236 | try:
237 | if len(msg) != 1:
238 | await sv.bot.send_group_msg(group_id=gid, message='\n======\n'.join(msg) + f'\n浏览{public_addr}查看详情')
239 | await asyncio.sleep(5)
240 | if len(msg2) != 1:
241 | await sv.bot.send_group_msg(group_id=gid, message='\n======\n'.join(msg2))
242 | await asyncio.sleep(5)
243 | except:
244 | continue
245 | await mai.get_music_alias()
246 | except (ServerError, ValueError) as e:
247 | log.error(str(e))
--------------------------------------------------------------------------------
/command/mai_base.py:
--------------------------------------------------------------------------------
1 | import random
2 | from re import Match
3 |
4 | from nonebot import NoneBot
5 | from PIL import Image
6 |
7 | from hoshino.service import priv
8 | from hoshino.typing import CQEvent, MessageSegment
9 |
10 | from .. import BOTNAME, Root, log, sv
11 | from ..libraries.image import image_to_base64, music_picture
12 | from ..libraries.maimaidx_api_data import maiApi
13 | from ..libraries.maimaidx_error import *
14 | from ..libraries.maimaidx_music import mai
15 | from ..libraries.maimaidx_music_info import draw_music_info
16 | from ..libraries.maimaidx_player_score import rating_ranking_data
17 | from ..libraries.tool import qqhash
18 |
19 | update_data = sv.on_fullmatch('更新maimai数据')
20 | maimaidxhelp = sv.on_fullmatch(['帮助maimaiDX', '帮助maimaidx'])
21 | maimaidxrepo = sv.on_fullmatch(['项目地址maimaiDX', '项目地址maimaidx'])
22 | mai_today = sv.on_prefix(['今日mai', '今日舞萌', '今日运势'])
23 | mai_what = sv.on_rex(r'.*mai.*什么(.+)?')
24 | random_song = sv.on_rex(r'^[来随给]个((?:dx|sd|标准))?([绿黄红紫白]?)([0-9]+\+?)$')
25 | rating_ranking = sv.on_prefix(['查看排名', '查看排行'])
26 | my_rating_ranking = sv.on_fullmatch('我的排名')
27 | data_update_daily = sv.scheduled_job('cron', hour='4')
28 |
29 |
30 | @update_data
31 | async def _(bot: NoneBot, ev: CQEvent):
32 | if not priv.check_priv(ev, priv.SUPERUSER):
33 | return
34 | await mai.get_music()
35 | await mai.get_music_alias()
36 | await bot.send(ev, 'maimai数据更新完成')
37 |
38 |
39 | @maimaidxhelp
40 | async def _(bot: NoneBot, ev: CQEvent):
41 | await bot.send(ev, MessageSegment.image(image_to_base64(Image.open((Root / 'maimaidxhelp.png')))), at_sender=True)
42 |
43 |
44 | @maimaidxrepo
45 | async def _(bot: NoneBot, ev: CQEvent):
46 | await bot.send(ev, f'项目地址:https://github.com/Yuri-YuzuChaN/maimaiDX\n求star,求宣传~', at_sender=True)
47 |
48 |
49 | @mai_today
50 | async def _(bot: NoneBot, ev: CQEvent):
51 | wm_list = ['拼机', '推分', '越级', '下埋', '夜勤', '练底力', '练手法', '打旧框', '干饭', '抓绝赞', '收歌']
52 | uid = ev.user_id
53 | h = qqhash(uid)
54 | rp = h % 100
55 | wm_value = []
56 | for i in range(11):
57 | wm_value.append(h & 3)
58 | h >>= 2
59 | msg = f'\n今日人品值:{rp}\n'
60 | for i in range(11):
61 | if wm_value[i] == 3:
62 | msg += f'宜 {wm_list[i]}\n'
63 | elif wm_value[i] == 0:
64 | msg += f'忌 {wm_list[i]}\n'
65 | music = mai.total_list[h % len(mai.total_list)]
66 | ds = '/'.join([str(_) for _ in music.ds])
67 | msg += f'{BOTNAME} Bot提醒您:打机时不要大力拍打或滑动哦\n今日推荐歌曲:\n'
68 | msg += f'ID.{music.id} - {music.title}'
69 | msg += MessageSegment.image(image_to_base64(Image.open(music_picture(music.id))))
70 | msg += ds
71 | await bot.send(ev, msg, at_sender=True)
72 |
73 |
74 | @mai_what
75 | async def _(bot: NoneBot, ev: CQEvent):
76 | match: Match[str] = ev['match']
77 | music = mai.total_list.random()
78 | user = None
79 | if (point := match.group(1)) and ('推分' in point or '上分' in point or '加分' in point):
80 | try:
81 | user = await maiApi.query_user_b50(qqid=ev.user_id)
82 | r = random.randint(0, 1)
83 | _ra = 0
84 | ignore = []
85 | if r == 0:
86 | if sd := user.charts.sd:
87 | ignore = [m.song_id for m in sd if m.achievements < 100.5]
88 | _ra = sd[-1].ra
89 | else:
90 | if dx := user.charts.dx:
91 | ignore = [m.song_id for m in dx if m.achievements < 100.5]
92 | _ra = dx[-1].ra
93 | if _ra != 0:
94 | ds = round(_ra / 22.4, 1)
95 | musiclist = mai.total_list.filter(ds=(ds, ds + 1))
96 | for _m in musiclist:
97 | if int(_m.id) in ignore:
98 | musiclist.remove(_m)
99 | music = musiclist.random()
100 | except (UserNotFoundError, UserDisabledQueryError):
101 | pass
102 | await bot.send(ev, await draw_music_info(music, ev.user_id, user))
103 |
104 |
105 | @random_song
106 | async def _(bot: NoneBot, ev: CQEvent):
107 | try:
108 | match: Match[str] = ev['match']
109 | diff = match.group(1)
110 | if diff == 'dx':
111 | tp = ['DX']
112 | elif diff == 'sd' or diff == '标准':
113 | tp = ['SD']
114 | else:
115 | tp = ['SD', 'DX']
116 | level = match.group(3)
117 | if match.group(2) == '':
118 | music_data = mai.total_list.filter(level=level, type=tp)
119 | else:
120 | music_data = mai.total_list.filter(level=level, diff=['绿黄红紫白'.index(match.group(2))], type=tp)
121 | if len(music_data) == 0:
122 | msg = '没有这样的乐曲哦。'
123 | else:
124 | msg = await draw_music_info(music_data.random(), ev.user_id)
125 | except:
126 | msg = '随机命令错误,请检查语法'
127 | await bot.send(ev, msg, at_sender=True)
128 |
129 |
130 | @rating_ranking
131 | async def _(bot: NoneBot, ev: CQEvent):
132 | args: str = ev.message.extract_plain_text().strip()
133 | page = 1
134 | name = ''
135 | if args.isdigit():
136 | page = int(args)
137 | else:
138 | name = args.lower()
139 |
140 | pic = await rating_ranking_data(name, page)
141 | await bot.send(ev, pic, at_sender=True)
142 |
143 |
144 | @my_rating_ranking
145 | async def _(bot: NoneBot, ev: CQEvent):
146 | try:
147 | user = await maiApi.query_user_b50(qqid=ev.user_id)
148 | rank_data = await maiApi.rating_ranking()
149 | for num, rank in enumerate(rank_data):
150 | if rank.username == user.username:
151 | result = f'您的Rating为「{rank.ra}」,排名第「{num + 1}」名'
152 | await bot.finish(ev, result, at_sender=True)
153 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
154 | await bot.finish(ev, str(e), at_sender=True)
155 |
156 |
157 |
158 | @data_update_daily
159 | async def _():
160 | await mai.get_music()
161 | mai.guess()
162 | log.info('maimaiDX数据更新完毕')
163 |
--------------------------------------------------------------------------------
/command/mai_guess.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from textwrap import dedent
3 |
4 | from nonebot import NoneBot
5 |
6 | from hoshino.service import priv
7 | from hoshino.typing import CQEvent, MessageSegment
8 |
9 | from .. import sv
10 | from ..libraries.maimaidx_music import guess
11 | from ..libraries.maimaidx_music_info import draw_music_info
12 |
13 | guess_music_start = sv.on_fullmatch('猜歌')
14 | guess_music_pic = sv.on_fullmatch('猜曲绘')
15 | guess_music_reset = sv.on_fullmatch('重置猜歌')
16 | guess_music_switch = sv.on_suffix('mai猜歌')
17 |
18 |
19 | @guess_music_start
20 | async def guess_music(bot: NoneBot, ev: CQEvent):
21 | gid = ev.group_id
22 | if ev.group_id not in guess.switch.enable:
23 | await bot.finish(ev, '该群已关闭猜歌功能,开启请输入 开启mai猜歌')
24 | if gid in guess.Group:
25 | await bot.finish(ev, '该群已有正在进行的猜歌或猜曲绘')
26 | guess.start(gid)
27 | await bot.send(ev, dedent(''' \
28 | 我将从热门乐曲中选择一首歌,每隔8秒描述它的特征,
29 | 请输入歌曲的 id 标题 或 别名(需bot支持,无需大小写) 进行猜歌(DX乐谱和标准乐谱视为两首歌)。
30 | 猜歌时查歌等其他命令依然可用。
31 | '''))
32 | await asyncio.sleep(4)
33 | for cycle in range(7):
34 | if ev.group_id not in guess.switch.enable or gid not in guess.Group or guess.Group[gid].end:
35 | break
36 | if cycle < 6:
37 | await bot.send(ev, f'{cycle + 1}/7 这首歌{guess.Group[gid].options[cycle]}')
38 | await asyncio.sleep(8)
39 | else:
40 | await bot.send(ev, f'''7/7 这首歌封面的一部分是:\n{MessageSegment.image(guess.Group[gid].img)}答案将在30秒后揭晓''')
41 | for _ in range(30):
42 | await asyncio.sleep(1)
43 | if gid in guess.Group:
44 | if ev.group_id not in guess.switch.enable or guess.Group[gid].end:
45 | return
46 | else:
47 | return
48 | guess.Group[gid].end = True
49 | answer = f'''答案是:\n{await draw_music_info(guess.Group[gid].music)}'''
50 | guess.end(gid)
51 | await bot.send(ev, answer)
52 |
53 |
54 | @guess_music_pic
55 | async def guess_pic(bot: NoneBot, ev: CQEvent):
56 | gid = ev.group_id
57 | if ev.group_id not in guess.switch.enable:
58 | await bot.finish(ev, '该群已关闭猜歌功能,开启请输入 开启mai猜歌')
59 | if gid in guess.Group:
60 | await bot.finish(ev, '该群已有正在进行的猜歌或猜曲绘')
61 | guess.startpic(gid)
62 | await bot.send(ev, f'以下裁切图片是哪首谱面的曲绘:\n{MessageSegment.image(guess.Group[gid].img)}请在30s内输入答案')
63 | for _ in range(30):
64 | await asyncio.sleep(1)
65 | if gid in guess.Group:
66 | if ev.group_id not in guess.switch.enable or guess.Group[gid].end:
67 | return
68 | else:
69 | return
70 | guess.Group[gid].end = True
71 | answer = f'''答案是:\n{await draw_music_info(guess.Group[gid].music)}'''
72 | guess.end(gid)
73 | await bot.send(ev, answer)
74 |
75 |
76 | @sv.on_message()
77 | async def guess_music_solve(bot: NoneBot, ev: CQEvent):
78 | gid = ev.group_id
79 | if gid not in guess.Group:
80 | return
81 | ans: str = ev.message.extract_plain_text().strip().lower()
82 | if ans.lower() in guess.Group[gid].answer:
83 | guess.Group[gid].end = True
84 | answer = f'''猜对了,答案是:\n{await draw_music_info(guess.Group[gid].music)}'''
85 | guess.end(gid)
86 | await bot.send(ev, answer, at_sender=True)
87 |
88 |
89 | @guess_music_reset
90 | async def reset_guess(bot: NoneBot, ev: CQEvent):
91 | gid = ev.group_id
92 | if not priv.check_priv(ev, priv.ADMIN):
93 | msg = '仅允许管理员重置'
94 | elif gid in guess.Group:
95 | msg = '已重置该群猜歌'
96 | guess.end(gid)
97 | else:
98 | msg = '该群未处在猜歌状态'
99 | await bot.send(ev, msg)
100 |
101 |
102 | @guess_music_switch
103 | async def guess_on_off(bot: NoneBot, ev: CQEvent):
104 | gid = ev.group_id
105 | args: str = ev.message.extract_plain_text().strip()
106 | if not priv.check_priv(ev, priv.ADMIN):
107 | msg = '仅允许管理员开关'
108 | elif args == '开启':
109 | msg = await guess.on(gid)
110 | elif args == '关闭':
111 | msg = await guess.off(gid)
112 | else:
113 | msg = '指令错误'
114 | await bot.send(ev, msg, at_sender=True)
--------------------------------------------------------------------------------
/command/mai_score.py:
--------------------------------------------------------------------------------
1 | import re
2 | from textwrap import dedent
3 |
4 | from nonebot import NoneBot
5 |
6 | from hoshino.typing import CQEvent, MessageSegment
7 |
8 | from .. import log, sv
9 | from ..libraries.image import image_to_base64, text_to_image
10 | from ..libraries.maimai_best_50 import generate
11 | from ..libraries.maimaidx_music import mai
12 | from ..libraries.maimaidx_music_info import draw_music_play_data
13 | from ..libraries.maimaidx_player_score import music_global_data
14 |
15 | best50 = sv.on_prefix(['b50', 'B50'])
16 | minfo = sv.on_prefix(['minfo', 'Minfo', 'MINFO', 'info', 'Info', 'INFO'])
17 | ginfo = sv.on_prefix(['ginfo', 'Ginfo', 'GINFO'])
18 | score = sv.on_prefix(['分数线'])
19 |
20 |
21 | @best50
22 | async def _(bot: NoneBot, ev: CQEvent):
23 | qqid = ev.user_id
24 | username: str = ev.message.extract_plain_text().strip()
25 | for i in ev.message:
26 | if i.type == 'at' and i.data['qq'] != 'all':
27 | qqid = int(i.data['qq'])
28 | await bot.send(ev, await generate(qqid, username), at_sender=True)
29 |
30 |
31 | @minfo
32 | async def _(bot: NoneBot, ev: CQEvent):
33 | qqid = ev.user_id
34 | args: str = ev.message.extract_plain_text().strip().lower()
35 | for i in ev.message:
36 | if i.type == 'at' and i.data['qq'] != 'all':
37 | qqid = int(i.data['qq'])
38 | if not args:
39 | await bot.finish(ev, '请输入曲目id或曲名', at_sender=True)
40 |
41 | if mai.total_list.by_id(args):
42 | songs = args
43 | elif by_t := mai.total_list.by_title(args):
44 | songs = by_t.id
45 | else:
46 | alias = mai.total_alias_list.by_alias(args)
47 | if not alias:
48 | await bot.finish(ev, '未找到曲目', at_sender=True)
49 | elif len(alias) != 1:
50 | msg = f'找到相同别名的曲目,请使用以下ID查询:\n'
51 | for songs in alias:
52 | msg += f'{songs.SongID}:{songs.Name}\n'
53 | await bot.finish(ev, msg.strip(), at_sender=True)
54 | else:
55 | songs = str(alias[0].SongID)
56 | pic = await draw_music_play_data(qqid, songs)
57 | await bot.send(ev, pic, at_sender=True)
58 |
59 |
60 | @ginfo
61 | async def _(bot: NoneBot, ev: CQEvent):
62 | args: str = ev.message.extract_plain_text().strip().lower()
63 | if not args:
64 | await bot.finish(ev, '请输入曲目id或曲名', at_sender=True)
65 | if args[0] not in '绿黄红紫白':
66 | level_index = 3
67 | else:
68 | level_index = '绿黄红紫白'.index(args[0])
69 | args = args[1:].strip()
70 | if not args:
71 | await bot.finish(ev, '请输入曲目id或曲名', at_sender=True)
72 | if mai.total_list.by_id(args):
73 | id = args
74 | elif by_t := mai.total_list.by_title(args):
75 | id = by_t.id
76 | else:
77 | alias = mai.total_alias_list.by_alias(args)
78 | if not alias:
79 | await bot.finish(ev, '未找到曲目', at_sender=True)
80 | elif len(alias) != 1:
81 | msg = f'找到相同别名的曲目,请使用以下ID查询:\n'
82 | for songs in alias:
83 | msg += f'{songs.SongID}:{songs.Name}\n'
84 | await bot.finish(ev, msg.strip(), at_sender=True)
85 | else:
86 | id = str(alias[0].SongID)
87 |
88 | music = mai.total_list.by_id(id)
89 | if not music.stats:
90 | await bot.finish(ev, '该乐曲还没有统计信息', at_sender=True)
91 | if len(music.ds) == 4 and level_index == 4:
92 | await bot.finish(ev, '该乐曲没有这个等级', at_sender=True)
93 | if not music.stats[level_index]:
94 | await bot.finish(ev, '该等级没有统计信息', at_sender=True)
95 | stats = music.stats[level_index]
96 | info = dedent(f'''\
97 | 游玩次数:{round(stats.cnt)}
98 | 拟合难度:{stats.fit_diff:.2f}
99 | 平均达成率:{stats.avg:.2f}%
100 | 平均 DX 分数:{stats.avg_dx:.1f}
101 | 谱面成绩标准差:{stats.std_dev:.2f}''')
102 | await bot.send(ev, await music_global_data(music, level_index) + info, at_sender=True)
103 |
104 |
105 | @score
106 | async def _(bot: NoneBot, ev: CQEvent):
107 | args: str = ev.message.extract_plain_text().strip()
108 | pro = args.split()
109 | if len(pro) == 1 and pro[0] == '帮助':
110 | msg = dedent('''\
111 | 此功能为查找某首歌分数线设计。
112 | 命令格式:分数线 <难度+歌曲id> <分数线>
113 | 例如:分数线 紫799 100
114 | 命令将返回分数线允许的 TAP GREAT 容错以及 BREAK 50落等价的 TAP GREAT 数。
115 | 以下为 TAP GREAT 的对应表:
116 | GREAT/GOOD/MISS
117 | TAP 1/2.5/5
118 | HOLD 2/5/10
119 | SLIDE 3/7.5/15
120 | TOUCH 1/2.5/5
121 | BREAK 5/12.5/25(外加200落)''')
122 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(msg))), at_sender=True)
123 | else:
124 | try:
125 | result = re.search(r'([绿黄红紫白])\s?([0-9]+)', args)
126 | level_labels = ['绿', '黄', '红', '紫', '白']
127 | level_labels2 = ['Basic', 'Advanced', 'Expert', 'Master', 'Re:MASTER']
128 | level_index = level_labels.index(result.group(1))
129 | chart_id = result.group(2)
130 | line = float(pro[-1])
131 | music = mai.total_list.by_id(chart_id)
132 | chart = music.charts[level_index]
133 | tap = int(chart.notes.tap)
134 | slide = int(chart.notes.slide)
135 | hold = int(chart.notes.hold)
136 | touch = int(chart.notes.touch) if len(chart.notes) == 5 else 0
137 | brk = int(chart.notes.brk)
138 | total_score = tap * 500 + slide * 1500 + hold * 1000 + touch * 500 + brk * 2500
139 | break_bonus = 0.01 / brk
140 | break_50_reduce = total_score * break_bonus / 4
141 | reduce = 101 - line
142 | if reduce <= 0 or reduce >= 101:
143 | raise ValueError
144 | msg = dedent(f'''{music.title} {level_labels2[level_index]}
145 | 分数线 {line}% 允许的最多 TAP GREAT 数量为 {(total_score * reduce / 10000):.2f}(每个-{10000 / total_score:.4f}%),
146 | BREAK 50落(一共{brk}个)等价于 {(break_50_reduce / 100):.3f} 个 TAP GREAT(-{break_50_reduce / total_score * 100:.4f}%)''')
147 | await bot.send(ev, msg, at_sender=True)
148 | except (AttributeError, ValueError) as e:
149 | log.exception(e)
150 | await bot.send(ev, '格式错误,输入“分数线 帮助”以查看帮助信息', at_sender=True)
--------------------------------------------------------------------------------
/command/mai_search.py:
--------------------------------------------------------------------------------
1 | import re
2 | from re import Match
3 | from typing import List, Tuple
4 |
5 | from nonebot import NoneBot
6 |
7 | from hoshino.typing import CQEvent, MessageSegment
8 |
9 | from .. import SONGS_PER_PAGE, diffs, sv
10 | from ..libraries.image import image_to_base64, text_to_image
11 | from ..libraries.maimaidx_api_data import maiApi
12 | from ..libraries.maimaidx_error import *
13 | from ..libraries.maimaidx_model import AliasStatus
14 | from ..libraries.maimaidx_music import guess, mai
15 | from ..libraries.maimaidx_music_info import draw_music_info
16 |
17 | search_music = sv.on_prefix(['查歌', 'search'])
18 | search_base = sv.on_prefix(['定数查歌', 'search base'])
19 | search_bpm = sv.on_prefix(['bpm查歌', 'search bpm'])
20 | search_artist = sv.on_prefix(['曲师查歌', 'search artist'])
21 | search_charter = sv.on_prefix(['谱师查歌', 'search charter'])
22 | search_alias_song = sv.on_suffix(('是什么歌', '是啥歌'))
23 | query_chart = sv.on_rex(re.compile(r'^id\s?([0-9]+)$', re.IGNORECASE))
24 |
25 |
26 | def song_level(ds1: float, ds2: float) -> List[Tuple[str, str, float, str]]:
27 | """
28 | 查询定数范围内的乐曲
29 |
30 | Params:
31 | `ds1`: 定数下限
32 | `ds2`: 定数上限
33 | Return:
34 | `result`: 查询结果
35 | """
36 | result: List[Tuple[str, str, float, str]] = []
37 | music_data = mai.total_list.filter(ds=(ds1, ds2))
38 | for music in sorted(music_data, key=lambda x: int(x.id)):
39 | if int(music.id) >= 100000:
40 | continue
41 | for i in music.diff:
42 | result.append((music.id, music.title, music.ds[i], diffs[i]))
43 | return result
44 |
45 |
46 | @search_music
47 | async def _(bot: NoneBot, ev: CQEvent):
48 | name: str = ev.message.extract_plain_text().strip()
49 | page = 1
50 | if not name:
51 | await bot.finish(ev, '请输入关键词', at_sender=True)
52 | result = mai.total_list.filter(title_search=name)
53 | if len(result) == 0:
54 | await bot.finish(ev, '没有找到这样的乐曲。\n※ 如果是别名请使用「xxx是什么歌」指令来查询哦。', at_sender=True)
55 | if len(result) == 1:
56 | await bot.finish(ev, await draw_music_info(result.random(), ev.user_id))
57 |
58 | search_result = ''
59 | result.sort(key=lambda i: int(i.id))
60 | for i, music in enumerate(result):
61 | if (page - 1) * SONGS_PER_PAGE <= i < page * SONGS_PER_PAGE:
62 | search_result += f'{f"「{music.id}」":<7} {music.title}\n'
63 | search_result += f'第「{page}」页,共「{len(result) // SONGS_PER_PAGE + 1}」页。请使用「id xxxxx」查询指定曲目。'
64 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(search_result))), at_sender=True)
65 |
66 |
67 | @search_base
68 | async def _(bot: NoneBot, ev: CQEvent):
69 | args: List[str] = ev.message.extract_plain_text().strip().split()
70 | if len(args) > 3 or len(args) == 0:
71 | await bot.finish(ev, '命令格式为\n定数查歌 「定数」「页数」\n定数查歌 「定数下限」「定数上限」「页数」', at_sender=True)
72 | page = 1
73 | if len(args) == 1:
74 | ds1, ds2 = args[0], args[0]
75 | elif len(args) == 2:
76 | if '.' in args[1]:
77 | ds1, ds2 = args
78 | else:
79 | ds1, ds2 = args[0], args[0]
80 | page = args[1]
81 | else:
82 | ds1, ds2, page = args
83 | page = int(page)
84 | result = song_level(float(ds1), float(ds2))
85 | if not result:
86 | await bot.finish(ev, f'没有找到这样的乐曲。', at_sender=True)
87 |
88 | search_result = ''
89 | for i, _result in enumerate(result):
90 | id, title, ds, diff = _result
91 | if (page - 1) * SONGS_PER_PAGE <= i < page * SONGS_PER_PAGE:
92 | search_result += f'{f"「{id}」":<7}{f"「{diff}」":<11}{f"「{ds}」"} {title}\n'
93 | search_result += f'第「{page}」页,共「{len(result) // SONGS_PER_PAGE + 1}」页。请使用「id xxxxx」查询指定曲目。'
94 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(search_result))), at_sender=True)
95 |
96 |
97 | @search_bpm
98 | async def search_dx_song_bpm(bot: NoneBot, ev: CQEvent):
99 | if str(ev.group_id) in guess.Group:
100 | await bot.finish(ev, '本群正在猜歌,不要作弊哦~', at_sender=True)
101 | args = ev.message.extract_plain_text().strip().split()
102 | page = 1
103 | if len(args) == 1:
104 | result = mai.total_list.filter(bpm=int(args[0]))
105 | elif len(args) == 2:
106 | if (bpm := int(args[0])) > int(args[1]):
107 | page = int(args[1])
108 | result = mai.total_list.filter(bpm=bpm)
109 | else:
110 | result = mai.total_list.filter(bpm=(bpm, int(args[1])))
111 | elif len(args) == 3:
112 | result = mai.total_list.filter(bpm=(int(args[0]), int(args[1])))
113 | page = int(args[2])
114 | else:
115 | await bot.finish(ev, '命令格式:\nbpm查歌 「bpm」\nbpm查歌 「bpm下限」「bpm上限」「页数」', at_sender=True)
116 | if not result:
117 | await bot.finish(ev, f'没有找到这样的乐曲。', at_sender=True)
118 |
119 | search_result = ''
120 | page = max(min(page, len(result) // SONGS_PER_PAGE + 1), 1)
121 | result.sort(key=lambda x: int(x.basic_info.bpm))
122 |
123 | for i, m in enumerate(result):
124 | if (page - 1) * SONGS_PER_PAGE <= i < page * SONGS_PER_PAGE:
125 | search_result += f'{f"「{m.id}」":<7}{f"「BPM {m.basic_info.bpm}」":<9} {m.title} \n'
126 | search_result += f'第「{page}」页,共「{len(result) // SONGS_PER_PAGE + 1}」页。请使用「id xxxxx」查询指定曲目。'
127 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(search_result))), at_sender=True)
128 |
129 |
130 | @search_artist
131 | async def search_dx_song_artist(bot: NoneBot, ev: CQEvent):
132 | if str(ev.group_id) in guess.Group:
133 | await bot.finish(ev, '本群正在猜歌,不要作弊哦~', at_sender=True)
134 | args: List[str] = ev.message.extract_plain_text().strip().split()
135 | page = 1
136 | if len(args) == 1:
137 | name: str = args[0]
138 | elif len(args) == 2:
139 | name: str = args[0]
140 | if args[1].isdigit():
141 | page = int(args[1])
142 | else:
143 | await bot.finish(ev, '命令格式:\n曲师查歌「曲师名称」「页数」', at_sender=True)
144 | else:
145 | await bot.finish(ev, '命令格式:\n曲师查歌「曲师名称」「页数」', at_sender=True)
146 |
147 | result = mai.total_list.filter(artist_search=name)
148 | if not result:
149 | await bot.finish(ev, f'没有找到这样的乐曲。', at_sender=True)
150 |
151 | search_result = ''
152 | page = max(min(page, len(result) // SONGS_PER_PAGE + 1), 1)
153 | for i, m in enumerate(result):
154 | if (page - 1) * SONGS_PER_PAGE <= i < page * SONGS_PER_PAGE:
155 | search_result += f'{f"「{m.id}」":<7}{f"「{m.basic_info.artist}」"} - {m.title}\n'
156 | search_result += f'第「{page}」页,共「{len(result) // SONGS_PER_PAGE + 1}」页。请使用「id xxxxx」查询指定曲目。'
157 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(search_result))), at_sender=True)
158 |
159 |
160 | @search_charter
161 | async def search_dx_song_charter(bot: NoneBot, ev: CQEvent):
162 | if str(ev.group_id) in guess.Group:
163 | await bot.finish(ev, '本群正在猜歌,不要作弊哦~', at_sender=True)
164 | args: List[str] = ev.message.extract_plain_text().strip().split()
165 | page = 1
166 | if len(args) == 1:
167 | name: str = args[0]
168 | elif len(args) == 2:
169 | name: str = args[0]
170 | if args[1].isdigit():
171 | page = int(args[1])
172 | else:
173 | await bot.finish(ev, '命令格式:\n谱师查歌「谱师名称」「页数」', at_sender=True)
174 | else:
175 | await bot.finish(ev, '命令格式:\n谱师查歌「谱师名称」「页数」', at_sender=True)
176 |
177 | result = mai.total_list.filter(charter_search=name)
178 | if not result:
179 | await bot.finish(ev, f'没有找到这样的乐曲。', at_sender=True)
180 |
181 | search_result = ''
182 | page = max(min(page, len(result) // SONGS_PER_PAGE + 1), 1)
183 | for i, m in enumerate(result):
184 | if (page - 1) * SONGS_PER_PAGE <= i < page * SONGS_PER_PAGE:
185 | diff_charter = zip([diffs[d] for d in m.diff], [m.charts[d].charter for d in m.diff])
186 | search_result += f'''{f"「{m.id}」":<7}{" ".join([f"{f'「{d}」':<9}{f'「{c}」'}" for d, c in diff_charter])} {m.title}\n'''
187 | search_result += f'第「{page}」页,共「{len(result) // SONGS_PER_PAGE + 1}」页。请使用「id xxxxx」查询指定曲目。'
188 | await bot.send(ev, MessageSegment.image(image_to_base64(text_to_image(search_result))), at_sender=True)
189 |
190 |
191 | @search_alias_song
192 | async def _(bot: NoneBot, ev: CQEvent):
193 | name: str = ev.message.extract_plain_text().strip().lower()
194 | error_msg = f'未找到别名为「{name}」的歌曲\n※ 可以使用「添加别名」指令给该乐曲添加别名\n※ 如果是歌名的一部分,请使用「查歌」指令查询哦。'
195 | # 别名
196 | alias_data = mai.total_alias_list.by_alias(name)
197 | if not alias_data:
198 | try:
199 | obj = await maiApi.get_songs(name)
200 | if obj:
201 | if type(obj[0]) == AliasStatus:
202 | msg = f'未找到别名为「{name}」的歌曲,但找到与此相同别名的投票:\n'
203 | for _s in obj:
204 | msg += f'- {_s.Tag}\n ID {_s.SongID}: {name}\n'
205 | msg += f'※ 可以使用指令「同意别名 {_s.Tag}」进行投票'
206 | await bot.finish(ev, msg.strip(), at_sender=True)
207 | else:
208 | alias_data = obj
209 | except AliasesNotFoundError:
210 | pass
211 | if alias_data:
212 | if len(alias_data) != 1:
213 | msg = f'找到{len(alias_data)}个相同别名的曲目:\n'
214 | for songs in alias_data:
215 | msg += f'{songs.SongID}:{songs.Name}\n'
216 | msg += '※ 请使用「id xxxxx」查询指定曲目'
217 | await bot.finish(ev, msg.strip(), at_sender=True)
218 | else:
219 | music = mai.total_list.by_id(str(alias_data[0].SongID))
220 | if music:
221 | msg = '您要找的是不是:' + (await draw_music_info(music, ev.user_id))
222 | else:
223 | msg = error_msg
224 | await bot.finish(ev, msg, at_sender=True)
225 |
226 | # id
227 | if name.isdigit() and (music := mai.total_list.by_id(name)):
228 | await bot.finish(ev, '您要找的是不是:' + (await draw_music_info(music, ev.user_id)), at_sender=True)
229 | if search_id := re.search(r'^id([0-9]*)$', name, re.IGNORECASE):
230 | music = mai.total_list.by_id(search_id.group(1))
231 | await bot.finish(ev, '您要找的是不是:' + (await draw_music_info(music, ev.user_id)), at_sender=True)
232 |
233 | # 标题
234 | result = mai.total_list.filter(title_search=name)
235 | if len(result) == 0:
236 | await bot.finish(ev, error_msg, at_sender=True)
237 | elif len(result) == 1:
238 | msg = await draw_music_info(result.random(), ev.user_id)
239 | await bot.finish(ev, '您要找的是不是:' + await draw_music_info(result.random(), ev.user_id), at_sender=True)
240 | elif len(result) < 50:
241 | msg = f'未找到别名为「{name}」的歌曲,但找到「{len(result)}」个相似标题的曲目:\n'
242 | for music in sorted(result, key=lambda x: int(x.id)):
243 | msg += f'{f"「{music.id}」":<7} {music.title}\n'
244 | msg += '请使用「id xxxxx」查询指定曲目。'
245 | await bot.finish(ev, msg.strip(), at_sender=True)
246 | else:
247 | await bot.finish(ev, f'结果过多「{len(result)}」条,请缩小查询范围。', at_sender=True)
248 |
249 |
250 | @query_chart
251 | async def _(bot: NoneBot, ev: CQEvent):
252 | match: Match[str] = ev['match']
253 | id = match.group(1)
254 | music = mai.total_list.by_id(id)
255 | if not music:
256 | msg = f'未找到ID为「{id}」的乐曲'
257 | else:
258 | msg = await draw_music_info(music, ev.user_id)
259 | await bot.send(ev, msg)
--------------------------------------------------------------------------------
/command/mai_table.py:
--------------------------------------------------------------------------------
1 | import re
2 | from re import Match
3 |
4 | from nonebot import NoneBot
5 |
6 | from hoshino.typing import CQEvent
7 |
8 | from .. import *
9 | from ..libraries.maimaidx_music_info import (
10 | draw_plate_table,
11 | draw_rating,
12 | draw_rating_table,
13 | )
14 | from ..libraries.maimaidx_player_score import (
15 | level_achievement_list_data,
16 | level_process_data,
17 | player_plate_data,
18 | rise_score_data,
19 | )
20 | from ..libraries.maimaidx_update_table import update_plate_table, update_rating_table
21 |
22 | update_table = sv.on_fullmatch('更新定数表')
23 | update_plate = sv.on_fullmatch('更新完成表')
24 | rating_table = sv.on_suffix('定数表')
25 | table_pfm = sv.on_suffix('完成表')
26 | rise_score = sv.on_rex(r'^我要在?([0-9]+\+?)?[上加\+]([0-9]+)?分\s?(.+)?')
27 | plate_process = sv.on_rex(r'^([真超檄橙暁晓桃櫻樱紫菫堇白雪輝辉舞霸熊華华爽煌宙星祭祝双宴])([極极将舞神者]舞?)进度\s?(.+)?')
28 | level_process = sv.on_rex(r'^([0-9]+\+?)\s?([abcdsfxp\+]+)\s?([\u4e00-\u9fa5]+)?进度\s?([0-9]+)?\s?(.+)?')
29 | level_achievement_list = sv.on_rex(r'^([0-9]+\.?[0-9]?\+?)分数列表\s?([0-9]+)?\s?(.+)?')
30 |
31 |
32 | @update_table
33 | async def _(bot: NoneBot, ev: CQEvent):
34 | if not priv.check_priv(ev, priv.SUPERUSER):
35 | return
36 | await bot.send(ev, await update_rating_table())
37 |
38 |
39 | @update_plate
40 | async def _(bot: NoneBot, ev: CQEvent):
41 | if not priv.check_priv(ev, priv.SUPERUSER):
42 | return
43 | await bot.send(ev, await update_plate_table())
44 |
45 |
46 | @rating_table
47 | async def _(bot: NoneBot, ev: CQEvent):
48 | args: str = ev.message.extract_plain_text().strip()
49 | if args in levelList[:5]:
50 | await bot.send(ev, '只支持查询lv6-15的定数表', at_sender=True)
51 | elif args in levelList[5:]:
52 | path = ratingdir / f'{args}.png'
53 | pic = draw_rating(args, path)
54 | await bot.send(ev, pic)
55 | else:
56 | await bot.send(ev, '无法识别的定数', at_sender=True)
57 |
58 |
59 | @table_pfm
60 | async def _(bot: NoneBot, ev: CQEvent):
61 | qqid = ev.user_id
62 | args: str = ev.message.extract_plain_text().strip()
63 | rating = re.search(r'^([0-9]+\+?)(app|fcp|ap|fc)?', args, re.IGNORECASE)
64 | plate = re.search(r'^([真超檄橙暁晓桃櫻樱紫菫堇白雪輝辉熊華华爽煌舞霸宙星祭祝双宴])([極极将舞神者]舞?)$', args)
65 | if rating:
66 | ra = rating.group(1)
67 | plan = rating.group(2)
68 | if args in levelList[:5]:
69 | await bot.send(ev, '只支持查询lv6-15的完成表', at_sender=True)
70 | elif ra in levelList[5:]:
71 | pic = await draw_rating_table(qqid, ra, True if plan and plan.lower() in combo_rank else False)
72 | await bot.send(ev, pic, at_sender=True)
73 | else:
74 | await bot.send(ev, '无法识别的表格', at_sender=True)
75 | elif plate:
76 | ver = plate.group(1)
77 | plan = plate.group(2)
78 | if ver in platecn:
79 | ver = platecn[ver]
80 | if ver in ['舞', '霸']:
81 | await bot.finish(ev, '暂不支持查询「舞」系和「霸者」的牌子', at_sender=True)
82 | if f'{ver}{plan}' == '真将':
83 | await bot.finish(ev, '真系没有真将哦', at_sender=True)
84 | pic = await draw_plate_table(qqid, ver, plan)
85 | await bot.send(ev, pic, at_sender=True)
86 | else:
87 | await bot.send(ev, '无法识别的表格', at_sender=True)
88 |
89 |
90 | @rise_score
91 | async def _(bot: NoneBot, ev: CQEvent):
92 | qqid = ev.user_id
93 | match: Match[str] = ev['match']
94 | username = None
95 | score = 0
96 | for i in ev.message:
97 | if i.type == 'at' and i.data['qq'] != 'all':
98 | qqid = int(i.data['qq'])
99 |
100 | if not match:
101 | rating = None
102 | score = None
103 | else:
104 | rating = match.group(1)
105 | if match.group(2):
106 | score = int(match.group(2))
107 |
108 | if rating and rating not in levelList:
109 | await bot.finish(ev, '无此等级', at_sender=True)
110 | if match.group(3):
111 | username = match.group(3).strip()
112 | if username:
113 | qqid = None
114 |
115 | data = await rise_score_data(qqid, username, rating, score)
116 | await bot.send(ev, data, at_sender=True)
117 |
118 |
119 | @plate_process
120 | async def _(bot: NoneBot, ev: CQEvent):
121 | qqid = ev.user_id
122 | match: Match[str] = ev['match']
123 | username = ''
124 | for i in ev.message:
125 | if i.type == 'at' and i.data['qq'] != 'all':
126 | qqid = int(i.data['qq'])
127 |
128 | ver = match.group(1)
129 | plan = match.group(2)
130 | if f'{ver}{plan}' == '真将':
131 | await bot.finish(ev, '真系没有真将哦', at_sender=True)
132 | elif match.group(3):
133 | username = match.group(3).strip()
134 | if username:
135 | qqid = None
136 |
137 | data = await player_plate_data(qqid, username, ver, plan)
138 | await bot.send(ev, data, at_sender=True)
139 |
140 |
141 | @level_process
142 | async def _(bot: NoneBot, ev: CQEvent):
143 | qqid = ev.user_id
144 | match: Match[str] = ev['match']
145 | username = ''
146 | for i in ev.message:
147 | if i.type == 'at' and i.data['qq'] != 'all':
148 | qqid = int(i.data['qq'])
149 |
150 | level = match.group(1)
151 | plan = match.group(2)
152 | category = match.group(3)
153 | page = match.group(4)
154 | username = match.group(5)
155 | if level not in levelList:
156 | await bot.finish(ev, '无此等级', at_sender=True)
157 | if plan.lower() not in scoreRank + comboRank + syncRank:
158 | await bot.finish(ev, '无此评价等级', at_sender=True)
159 | if levelList.index(level) < 11 or (plan.lower() in scoreRank and scoreRank.index(plan.lower()) < 8):
160 | await bot.finish(ev, '兄啊,有点志向好不好', at_sender=True)
161 | if category:
162 | if category in ['已完成', '未完成', '未开始', '未游玩']:
163 | _c = {
164 | '已完成': 'completed',
165 | '未完成': 'unfinished',
166 | '未开始': 'notstarted',
167 | '未游玩': 'notstarted'
168 | }
169 | category = _c[category]
170 | else:
171 | await bot.finish(ev, f'无法指定查询「{category}」', at_sender=True)
172 | else:
173 | category = 'default'
174 |
175 | data = await level_process_data(qqid, username, level, plan, category, int(page) if page else 1)
176 | await bot.send(ev, data, at_sender=True)
177 |
178 |
179 | @level_achievement_list
180 | async def _(bot: NoneBot, ev: CQEvent):
181 | qqid = ev.user_id
182 | match: Match[str] = ev['match']
183 | username = ''
184 | for i in ev.message:
185 | if i.type == 'at' and i.data['qq'] != 'all':
186 | qqid = int(i.data['qq'])
187 |
188 | rating = match.group(1)
189 | page = match.group(2)
190 | username = match.group(3)
191 |
192 | try:
193 | if '.' in rating:
194 | rating = round(float(rating), 1)
195 | elif rating not in levelList:
196 | await bot.finish(ev, '无此等级', at_sender=True)
197 | except ValueError:
198 | if rating not in levelList:
199 | await bot.finish(ev, '无此等级', at_sender=True)
200 |
201 | data = await level_achievement_list_data(qqid, username, rating, int(page) if page else 1)
202 | await bot.send(ev, data, at_sender=True)
--------------------------------------------------------------------------------
/libraries/image.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from io import BytesIO
3 | from typing import Tuple, Union
4 |
5 | import numpy as np
6 | from PIL import Image, ImageDraw, ImageFont, ImageOps
7 |
8 | from .. import SHANGGUMONO, Path, coverdir
9 |
10 |
11 | class DrawText:
12 |
13 | def __init__(self, image: ImageDraw.ImageDraw, font: Path) -> None:
14 | self._img = image
15 | self._font = str(font)
16 |
17 | def get_box(self, text: str, size: int) -> Tuple[float, float, float, float]:
18 | return ImageFont.truetype(self._font, size).getbbox(text)
19 |
20 | def draw(
21 | self,
22 | pos_x: int,
23 | pos_y: int,
24 | size: int,
25 | text: Union[str, int, float],
26 | color: Tuple[int, int, int, int] = (255, 255, 255, 255),
27 | anchor: str = 'lt',
28 | stroke_width: int = 0,
29 | stroke_fill: Tuple[int, int, int, int] = (0, 0, 0, 0),
30 | multiline: bool = False
31 | ) -> None:
32 | font = ImageFont.truetype(self._font, size)
33 | if multiline:
34 | self._img.multiline_text(
35 | (pos_x, pos_y),
36 | str(text),
37 | color,
38 | font,
39 | anchor,
40 | stroke_width=stroke_width,
41 | stroke_fill=stroke_fill
42 | )
43 | else:
44 | self._img.text(
45 | (pos_x, pos_y),
46 | str(text),
47 | color,
48 | font,
49 | anchor,
50 | stroke_width=stroke_width,
51 | stroke_fill=stroke_fill
52 | )
53 |
54 |
55 | def tricolor_gradient(
56 | width: int,
57 | height: int,
58 | color1: Tuple[int, int, int] = (124, 129, 255),
59 | color2: Tuple[int, int, int] = (193, 247, 225),
60 | color3: Tuple[int, int, int] = (255, 255, 255)
61 | ) -> Image.Image:
62 | """绘制渐变色"""
63 | array = np.zeros((height, width, 3), dtype=np.uint8)
64 |
65 | for y in range(height):
66 | if y < height * 0.4:
67 | ratio = y / (height * 0.4)
68 | color = (1 - ratio) * np.array(color1) + ratio * np.array(color2)
69 | else:
70 | ratio = (y - height * 0.4) / (height * 0.6)
71 | color = (1 - ratio) * np.array(color2) + ratio * np.array(color3)
72 | array[y, :] = np.clip(color, 0, 255)
73 |
74 | image = Image.fromarray(array).convert('RGBA')
75 | return image
76 |
77 |
78 | def rounded_corners(
79 | image: Image.Image,
80 | radius: int,
81 | corners: Tuple[bool, bool, bool, bool] = (False, False, False, False)
82 | ) -> Image.Image:
83 | """
84 | 绘制圆角
85 |
86 | Params:
87 | `image`: `PIL.Image.Image`
88 | `radius`: 圆角半径
89 | `corners`: 四个角是否绘制圆角,分别是左上、右上、右下、左下
90 | Returns:
91 | `PIL.Image.Image`
92 | """
93 | mask = Image.new('L', image.size, 0)
94 | draw = ImageDraw.Draw(mask)
95 | draw.rounded_rectangle((0, 0, image.size[0], image.size[1]), radius, fill=255, corners=corners)
96 |
97 | new_im = ImageOps.fit(image, mask.size)
98 | new_im.putalpha(mask)
99 |
100 | return new_im
101 |
102 |
103 | def music_picture(music_id: Union[int, str]) -> Path:
104 | """
105 | 获取谱面图片路径
106 |
107 | Params:
108 | `music_id`: 谱面 ID
109 | Returns:
110 | `Path`
111 | """
112 | music_id = int(music_id)
113 | if (_path := coverdir / f'{music_id}.png').exists():
114 | return _path
115 | if music_id > 100000:
116 | music_id -= 100000
117 | if (_path := coverdir / f'{music_id}.png').exists():
118 | return _path
119 | if 1000 < music_id < 10000 or 10000 < music_id <= 11000:
120 | for _id in [music_id + 10000, music_id - 10000]:
121 | if (_path := coverdir / f'{_id}.png').exists():
122 | return _path
123 | return coverdir / '11000.png'
124 |
125 |
126 | def text_to_image(text: str) -> Image.Image:
127 | font = ImageFont.truetype(str(SHANGGUMONO), 24)
128 | padding = 10
129 | margin = 4
130 | lines = text.strip().split('\n')
131 | max_width = 0
132 | b = 0
133 | for line in lines:
134 | l, t, r, b = font.getbbox(line)
135 | max_width = max(max_width, r)
136 | wa = max_width + padding * 2
137 | ha = b * len(lines) + margin * (len(lines) - 1) + padding * 2
138 | im = Image.new('RGB', (wa, ha), color=(255, 255, 255))
139 | draw = ImageDraw.Draw(im)
140 | for index, line in enumerate(lines):
141 | draw.text((padding, padding + index * (margin + b)), line, font=font, fill=(0, 0, 0))
142 | return im
143 |
144 |
145 | def image_to_base64(img: Image.Image, format='PNG') -> str:
146 | output_buffer = BytesIO()
147 | img.save(output_buffer, format)
148 | byte_data = output_buffer.getvalue()
149 | base64_str = base64.b64encode(byte_data).decode()
150 | return 'base64://' + base64_str
--------------------------------------------------------------------------------
/libraries/maimai_best_50.py:
--------------------------------------------------------------------------------
1 | import math
2 | import traceback
3 | from io import BytesIO
4 | from typing import Optional, Tuple, Union, overload
5 |
6 | from PIL import Image, ImageDraw
7 |
8 | from hoshino.typing import MessageSegment
9 |
10 | from .. import *
11 | from .image import DrawText, image_to_base64, music_picture
12 | from .maimaidx_api_data import maiApi
13 | from .maimaidx_error import *
14 | from .maimaidx_model import ChartInfo, PlayInfoDefault, PlayInfoDev, UserInfo
15 | from .maimaidx_music import mai
16 |
17 |
18 | class ScoreBaseImage:
19 |
20 | text_color = (124, 129, 255, 255)
21 | t_color = [
22 | (255, 255, 255, 255),
23 | (255, 255, 255, 255),
24 | (255, 255, 255, 255),
25 | (255, 255, 255, 255),
26 | (138, 0, 226, 255)
27 | ]
28 | id_color = [
29 | (129, 217, 85, 255),
30 | (245, 189, 21, 255),
31 | (255, 129, 141, 255),
32 | (159, 81, 220, 255),
33 | (138, 0, 226, 255)
34 | ]
35 | bg_color = [
36 | (111, 212, 61, 255),
37 | (248, 183, 9, 255),
38 | (255, 129, 141, 255),
39 | (159, 81, 220, 255),
40 | (219, 170, 255, 255)
41 | ]
42 | id_diff = [Image.new('RGBA', (55, 10), color) for color in bg_color]
43 |
44 | _diff = []
45 | _rise = []
46 | title_bg = None
47 | title_lengthen_bg = None
48 | design_bg = None
49 | aurora_bg = None
50 | shines_bg = None
51 | pattern_bg = None
52 | rainbow_bg = None
53 | rainbow_bottom_bg = None
54 |
55 | @classmethod
56 | def _load_image(cls):
57 | cls._diff = [
58 | Image.open(maimaidir / 'b50_score_basic.png'),
59 | Image.open(maimaidir / 'b50_score_advanced.png'),
60 | Image.open(maimaidir / 'b50_score_expert.png'),
61 | Image.open(maimaidir / 'b50_score_master.png'),
62 | Image.open(maimaidir / 'b50_score_remaster.png')
63 | ]
64 | cls._rise = [
65 | Image.open(maimaidir / 'rise_score_basic.png'),
66 | Image.open(maimaidir / 'rise_score_advanced.png'),
67 | Image.open(maimaidir / 'rise_score_expert.png'),
68 | Image.open(maimaidir / 'rise_score_master.png'),
69 | Image.open(maimaidir / 'rise_score_remaster.png')
70 | ]
71 | cls.title_bg = Image.open(maimaidir / 'title.png')
72 | cls.title_lengthen_bg = Image.open(maimaidir / 'title-lengthen.png')
73 | cls.design_bg = Image.open(maimaidir / 'design.png')
74 | cls.aurora_bg = Image.open(maimaidir / 'aurora.png').convert('RGBA').resize((1400, 220))
75 | cls.shines_bg = Image.open(maimaidir / 'bg_shines.png').convert('RGBA')
76 | cls.pattern_bg = Image.open(maimaidir / 'pattern.png')
77 | cls.rainbow_bg = Image.open(maimaidir / 'rainbow.png').convert('RGBA')
78 | cls.rainbow_bottom_bg = Image.open(maimaidir / 'rainbow_bottom.png').convert('RGBA').resize((1200, 200))
79 |
80 |
81 | def __init__(self, image: Image.Image = None) -> None:
82 | if not maiApi.config.saveinmem:
83 | self._load_image()
84 | self._im = image
85 | dr = ImageDraw.Draw(self._im)
86 | self._sy = DrawText(dr, SIYUAN)
87 | self._tb = DrawText(dr, TBFONT)
88 |
89 | def whiledraw(
90 | self,
91 | data: Union[List[ChartInfo], List[PlayInfoDefault], List[PlayInfoDev]],
92 | best: bool,
93 | height: int = 0
94 | ) -> None:
95 | """
96 | 循环绘制成绩
97 |
98 | Params:
99 | `data`: 数据
100 | `dx`: 是否为新版本成绩
101 | `height`: 起始高度
102 | """
103 | # y为第一排纵向坐标,dy为各行间距
104 | dy = 114
105 | if data and type(data[0]) == ChartInfo:
106 | y = 235 if best else 1085
107 | else:
108 | y = height
109 | for num, info in enumerate(data):
110 | if num % 5 == 0:
111 | x = 16
112 | y += dy if num != 0 else 0
113 | else:
114 | x += 276
115 |
116 | cover = Image.open(music_picture(info.song_id)).resize((75, 75))
117 | version = Image.open(maimaidir / f'{info.type.upper()}.png').resize((37, 14))
118 | if info.rate.islower():
119 | rate = Image.open(maimaidir / f'UI_TTR_Rank_{score_Rank_l[info.rate]}.png').resize((63, 28))
120 | else:
121 | rate = Image.open(maimaidir / f'UI_TTR_Rank_{info.rate}.png').resize((63, 28))
122 |
123 | self._im.alpha_composite(self._diff[info.level_index], (x, y))
124 | self._im.alpha_composite(cover, (x + 12, y + 12))
125 | self._im.alpha_composite(version, (x + 51, y + 91))
126 | self._im.alpha_composite(rate, (x + 92, y + 78))
127 | if info.fc:
128 | fc = Image.open(maimaidir / f'UI_MSS_MBase_Icon_{fcl[info.fc]}.png').resize((34, 34))
129 | self._im.alpha_composite(fc, (x + 154, y + 77))
130 | if info.fs:
131 | fs = Image.open(maimaidir / f'UI_MSS_MBase_Icon_{fsl[info.fs]}.png').resize((34, 34))
132 | self._im.alpha_composite(fs, (x + 185, y + 77))
133 |
134 | dxscore = sum(mai.total_list.by_id(str(info.song_id)).charts[info.level_index].notes) * 3
135 | dxnum = dxScore(info.dxScore / dxscore * 100)
136 | if dxnum:
137 | self._im.alpha_composite(
138 | Image.open(maimaidir / f'UI_GAM_Gauge_DXScoreIcon_0{dxnum}.png').resize((47, 26)), (x + 217, y + 80)
139 | )
140 |
141 | self._tb.draw(x + 26, y + 98, 13, info.song_id, self.id_color[info.level_index], anchor='mm')
142 | title = info.title
143 | if coloumWidth(title) > 18:
144 | title = changeColumnWidth(title, 17) + '...'
145 | self._sy.draw(x + 93, y + 14, 14, title, self.t_color[info.level_index], anchor='lm')
146 | self._tb.draw(x + 93, y + 38, 30, f'{info.achievements:.4f}%', self.t_color[info.level_index], anchor='lm')
147 | self._tb.draw(x + 219, y + 65, 15, f'{info.dxScore}/{dxscore}', self.t_color[info.level_index], anchor='mm')
148 | self._tb.draw(x + 93, y + 65, 15, f'{info.ds} -> {info.ra}', self.t_color[info.level_index], anchor='lm')
149 |
150 |
151 | class DrawBest(ScoreBaseImage):
152 |
153 | def __init__(self, UserInfo: UserInfo, qqid: Optional[Union[int, str]] = None) -> None:
154 | super().__init__(Image.open(maimaidir / 'b50_bg.png').convert('RGBA'))
155 | self.userName = UserInfo.nickname
156 | self.plate = UserInfo.plate
157 | self.addRating = UserInfo.additional_rating
158 | self.Rating = UserInfo.rating
159 | self.sdBest = UserInfo.charts.sd
160 | self.dxBest = UserInfo.charts.dx
161 | self.qqid = qqid
162 |
163 | def _findRaPic(self) -> str:
164 | """
165 | 寻找指定的Rating图片
166 |
167 | Returns:
168 | `str` 返回图片名称
169 | """
170 | if self.Rating < 1000:
171 | num = '01'
172 | elif self.Rating < 2000:
173 | num = '02'
174 | elif self.Rating < 4000:
175 | num = '03'
176 | elif self.Rating < 7000:
177 | num = '04'
178 | elif self.Rating < 10000:
179 | num = '05'
180 | elif self.Rating < 12000:
181 | num = '06'
182 | elif self.Rating < 13000:
183 | num = '07'
184 | elif self.Rating < 14000:
185 | num = '08'
186 | elif self.Rating < 14500:
187 | num = '09'
188 | elif self.Rating < 15000:
189 | num = '10'
190 | else:
191 | num = '11'
192 | return f'UI_CMN_DXRating_{num}.png'
193 |
194 | def _findMatchLevel(self) -> str:
195 | """
196 | 寻找匹配等级图片
197 |
198 | Returns:
199 | `str` 返回图片名称
200 | """
201 | if self.addRating <= 10:
202 | num = f'{self.addRating:02d}'
203 | else:
204 | num = f'{self.addRating + 1:02d}'
205 | return f'UI_DNM_DaniPlate_{num}.png'
206 |
207 | async def draw(self) -> Image.Image:
208 |
209 | logo = Image.open(maimaidir / 'logo.png').resize((249, 120))
210 | dx_rating = Image.open(maimaidir / self._findRaPic()).resize((186, 35))
211 | Name = Image.open(maimaidir / 'Name.png')
212 | MatchLevel = Image.open(maimaidir / self._findMatchLevel()).resize((80, 32))
213 | ClassLevel = Image.open(maimaidir / 'UI_FBR_Class_00.png').resize((90, 54))
214 | rating = Image.open(maimaidir / 'UI_CMN_Shougou_Rainbow.png').resize((270, 27))
215 |
216 | self._im.alpha_composite(logo, (14, 60))
217 | if self.plate:
218 | plate = Image.open(platedir / f'{self.plate}.png').resize((800, 130))
219 | else:
220 | plate = Image.open(maimaidir / 'UI_Plate_300501.png').resize((800, 130))
221 | self._im.alpha_composite(plate, (300, 60))
222 | icon = Image.open(maimaidir / 'UI_Icon_309503.png').resize((120, 120))
223 | self._im.alpha_composite(icon, (305, 65))
224 | if self.qqid:
225 | try:
226 | qqLogo = Image.open(BytesIO(await maiApi.qqlogo(qqid=self.qqid)))
227 | self._im.alpha_composite(qqLogo.convert('RGBA').resize((120, 120)), (305, 65))
228 | except Exception:
229 | pass
230 | self._im.alpha_composite(dx_rating, (435, 72))
231 | Rating = f'{self.Rating:05d}'
232 | for n, i in enumerate(Rating):
233 | self._im.alpha_composite(
234 | Image.open(maimaidir / f'UI_NUM_Drating_{i}.png').resize((17, 20)), (520 + 15 * n, 80)
235 | )
236 | self._im.alpha_composite(Name, (435, 115))
237 | self._im.alpha_composite(MatchLevel, (625, 120))
238 | self._im.alpha_composite(ClassLevel, (620, 60))
239 | self._im.alpha_composite(rating, (435, 160))
240 |
241 | self._sy.draw(445, 135, 25, self.userName, (0, 0, 0, 255), 'lm')
242 | sdrating, dxrating = sum([_.ra for _ in self.sdBest]), sum([_.ra for _ in self.dxBest])
243 | self._tb.draw(
244 | 570, 172, 17,
245 | f'B35: {sdrating} + B15: {dxrating} = {self.Rating}',
246 | (0, 0, 0, 255), 'mm', 3, (255, 255, 255, 255)
247 | )
248 | self._sy.draw(
249 | 700, 1570, 27,
250 | f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} BOT',
251 | self.text_color, 'mm', 5, (255, 255, 255, 255)
252 | )
253 |
254 | self.whiledraw(self.sdBest, True)
255 | self.whiledraw(self.dxBest, False)
256 |
257 | return self._im
258 |
259 |
260 | def dxScore(dx: int) -> int:
261 | """
262 | 获取DX评分星星数量
263 |
264 | Params:
265 | `dx`: dx百分比
266 | Returns:
267 | `int` 返回星星数量
268 | """
269 | if dx <= 85:
270 | result = 0
271 | elif dx <= 90:
272 | result = 1
273 | elif dx <= 93:
274 | result = 2
275 | elif dx <= 95:
276 | result = 3
277 | elif dx <= 97:
278 | result = 4
279 | else:
280 | result = 5
281 | return result
282 |
283 |
284 | def getCharWidth(o: int) -> int:
285 | widths = [
286 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0),
287 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1),
288 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1),
289 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2),
290 | (120831, 1), (262141, 2), (1114109, 1),
291 | ]
292 | if o == 0xe or o == 0xf:
293 | return 0
294 | for num, wid in widths:
295 | if o <= num:
296 | return wid
297 | return 1
298 |
299 |
300 | def coloumWidth(s: str) -> int:
301 | res = 0
302 | for ch in s:
303 | res += getCharWidth(ord(ch))
304 | return res
305 |
306 |
307 | def changeColumnWidth(s: str, len: int) -> str:
308 | res = 0
309 | sList = []
310 | for ch in s:
311 | res += getCharWidth(ord(ch))
312 | if res <= len:
313 | sList.append(ch)
314 | return ''.join(sList)
315 |
316 |
317 | @overload
318 | def computeRa(ds: float, achievement: float) -> int:
319 | """
320 | 计算底分
321 |
322 | Params:
323 | `ds`: 定数
324 | `achievement`: 成绩
325 | Returns:
326 | 返回底分
327 | """
328 | @overload
329 | def computeRa(ds: float, achievement: float, *, onlyrate: bool = False) -> str:
330 | """
331 | 计算评价
332 |
333 | Params:
334 | `ds`: 定数
335 | `achievement`: 成绩
336 | `onlyrate`: 是否只返回评价
337 | Returns:
338 | 返回评价
339 | """
340 | @overload
341 | def computeRa(ds: float, achievement: float, *, israte: bool = False) -> Tuple[int, str]:
342 | """
343 | 计算底分和评价
344 |
345 | Params:
346 | `ds`: 定数
347 | `achievement`: 成绩
348 | `israte`: 是否返回所有数据
349 | Returns:
350 | (底分, 评价)
351 | """
352 | def computeRa(
353 | ds: float,
354 | achievement: float,
355 | *,
356 | onlyrate: bool = False,
357 | israte: bool = False
358 | ) -> Union[int, Tuple[int, str]]:
359 | if achievement < 50:
360 | baseRa = 7.0
361 | rate = 'D'
362 | elif achievement < 60:
363 | baseRa = 8.0
364 | rate = 'C'
365 | elif achievement < 70:
366 | baseRa = 9.6
367 | rate = 'B'
368 | elif achievement < 75:
369 | baseRa = 11.2
370 | rate = 'BB'
371 | elif achievement < 80:
372 | baseRa = 12.0
373 | rate = 'BBB'
374 | elif achievement < 90:
375 | baseRa = 13.6
376 | rate = 'A'
377 | elif achievement < 94:
378 | baseRa = 15.2
379 | rate = 'AA'
380 | elif achievement < 97:
381 | baseRa = 16.8
382 | rate = 'AAA'
383 | elif achievement < 98:
384 | baseRa = 20.0
385 | rate = 'S'
386 | elif achievement < 99:
387 | baseRa = 20.3
388 | rate = 'Sp'
389 | elif achievement < 99.5:
390 | baseRa = 20.8
391 | rate = 'SS'
392 | elif achievement < 100:
393 | baseRa = 21.1
394 | rate = 'SSp'
395 | elif achievement < 100.5:
396 | baseRa = 21.6
397 | rate = 'SSS'
398 | else:
399 | baseRa = 22.4
400 | rate = 'SSSp'
401 |
402 | if israte:
403 | data = (math.floor(ds * (min(100.5, achievement) / 100) * baseRa), rate)
404 | elif onlyrate:
405 | data = rate
406 | else:
407 | data = math.floor(ds * (min(100.5, achievement) / 100) * baseRa)
408 |
409 | return data
410 |
411 |
412 | async def generate(qqid: Optional[int] = None, username: Optional[str] = None) -> Union[MessageSegment, str]:
413 | """
414 | 生成b50
415 |
416 | Params:
417 | `qqid`: QQ号
418 | `username`: 用户名
419 | `icon`: 头像
420 | Returns:
421 | `Union[MessageSegment, str]`
422 | """
423 | try:
424 | if username:
425 | qqid = None
426 | userinfo = await maiApi.query_user_b50(qqid=qqid, username=username)
427 | draw_best = DrawBest(userinfo, qqid)
428 |
429 | msg = MessageSegment.image(image_to_base64(await draw_best.draw()))
430 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
431 | msg = str(e)
432 | except Exception as e:
433 | log.error(traceback.format_exc())
434 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
435 | return msg
--------------------------------------------------------------------------------
/libraries/maimaidx_api_data.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Any, Dict
3 |
4 | from aiohttp import ClientSession, ClientTimeout
5 |
6 | from .. import config_json
7 | from .maimaidx_error import *
8 | from .maimaidx_model import *
9 |
10 |
11 | class MaiConfig(BaseModel):
12 |
13 | maimaidxtoken: Optional[str] = None
14 | maimaidxproberproxy: bool = False
15 | maimaidxaliasproxy: bool = False
16 | saveinmem: Optional[bool] = True
17 |
18 |
19 | class MaimaiAPI:
20 |
21 | MaiProxyAPI = 'https://proxy.yuzuchan.xyz'
22 |
23 | MaiProberAPI = 'https://www.diving-fish.com/api/maimaidxprober'
24 | MaiCover = 'https://www.diving-fish.com/covers'
25 | MaiAliasAPI = 'https://www.yuzuchan.moe/api/maimaidx'
26 | QQAPI = 'http://q1.qlogo.cn/g'
27 |
28 | def __init__(self) -> None:
29 | """封装Api"""
30 | self.config: MaiConfig = self.load_config()
31 | self.headers = None
32 | self.token = None
33 | self.MaiProberProxyAPI = None
34 | self.MaiAliasProxyAPI = None
35 |
36 | def load_config(self) -> MaiConfig:
37 | return MaiConfig.model_validate(json.load(open(config_json, 'r', encoding='utf-8')))
38 |
39 | def load_token_proxy(self) -> None:
40 | self.MaiProberProxyAPI = self.MaiProberAPI if not self.config.maimaidxproberproxy else self.MaiProxyAPI + '/maimaidxprober'
41 | self.MaiAliasProxyAPI = self.MaiAliasAPI if not self.config.maimaidxaliasproxy else self.MaiProxyAPI + '/maimaidxaliases'
42 | self.token = self.config.maimaidxtoken
43 | if self.token:
44 | self.headers = {'developer-token': self.token}
45 |
46 |
47 | async def _requestalias(self, method: str, endpoint: str, **kwargs) -> Any:
48 | """
49 | 别名库通用请求
50 |
51 | Params:
52 | `method`: 请求方式
53 | `endpoint`: 请求接口
54 | `kwargs`: 其它参数
55 | Returns:
56 | `Dict[str, Any]` 返回结果
57 | """
58 | async with ClientSession(timeout=ClientTimeout(total=30)) as session:
59 | async with session.request(method, self.MaiAliasProxyAPI + endpoint, **kwargs) as res:
60 | if res.status == 200:
61 | data = (await res.json())['content']
62 | if data == {} or data == []:
63 | raise AliasesNotFoundError
64 | if isinstance(data, str):
65 | return data
66 | elif res.status == 201:
67 | data = await res.json()
68 | elif res.status == 400:
69 | raise EnterError
70 | elif res.status == 500:
71 | raise ServerError
72 | else:
73 | raise UnknownError
74 | return data
75 |
76 | async def _requestmai(self, method: str, endpoint: str, **kwargs) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
77 | """
78 | 查分器通用请求
79 |
80 | Params:
81 | `method`: 请求方式
82 | `endpoint`: 请求接口
83 | `kwargs`: 其它参数
84 | Returns:
85 | `Dict[str, Any]` 返回结果
86 | """
87 | async with ClientSession(timeout=ClientTimeout(total=30)) as session:
88 | async with session.request(method, self.MaiProberProxyAPI + endpoint, headers=self.headers, **kwargs) as res:
89 | if res.status == 200:
90 | data = await res.json()
91 | elif res.status == 400:
92 | error: Dict = await res.json()
93 | if 'message' in error:
94 | if error['message'] == 'no such user':
95 | raise UserNotFoundError
96 | elif error['message'] == 'user not exists':
97 | raise UserNotExistsError
98 | else:
99 | raise UserNotFoundError
100 | elif 'msg' in error:
101 | if error['msg'] == '开发者token有误':
102 | raise TokenError
103 | elif error['msg'] == '开发者token被禁用':
104 | raise TokenDisableError
105 | else:
106 | raise TokenNotFoundError
107 | else:
108 | raise UserNotFoundError
109 | elif res.status == 403:
110 | raise UserDisabledQueryError
111 | else:
112 | raise UnknownError
113 | return data
114 |
115 | async def music_data(self):
116 | """获取曲目数据"""
117 | return await self._requestmai('GET', '/music_data')
118 |
119 | async def chart_stats(self):
120 | """获取单曲数据"""
121 | return await self._requestmai('GET', '/chart_stats')
122 |
123 | async def query_user_b50(self, *, qqid: Optional[int] = None, username: Optional[str] = None) -> UserInfo:
124 | """
125 | 获取玩家B50
126 |
127 | Params:
128 | `qqid`: QQ号
129 | `username`: 用户名
130 | Returns:
131 | `UserInfo` b50数据模型
132 | """
133 | json = {}
134 | if qqid:
135 | json['qq'] = qqid
136 | if username:
137 | json['username'] = username
138 | json['b50'] = True
139 |
140 | return UserInfo.model_validate(await self._requestmai('POST', '/query/player', json=json))
141 |
142 | async def query_user_plate(
143 | self,
144 | *,
145 | qqid: Optional[int] = None,
146 | username: Optional[str] = None,
147 | version: Optional[List[str]] = None
148 | ) -> List[PlayInfoDefault]:
149 | """
150 | 请求用户数据
151 |
152 | Params:
153 | `qqid`: 用户QQ
154 | `username`: 查分器用户名
155 | `version`: 版本
156 | Returns:
157 | `List[PlayInfoDefault]` 数据列表
158 | """
159 | json = {}
160 | if qqid:
161 | json['qq'] = qqid
162 | if username:
163 | json['username'] = username
164 | if version:
165 | json['version'] = version
166 | result = await self._requestmai('POST', '/query/plate', json=json)
167 | if not result['verlist']:
168 | raise MusicNotPlayError
169 |
170 | return [PlayInfoDefault.model_validate(d) for d in result['verlist']]
171 |
172 | async def query_user_get_dev(self, *, qqid: Optional[int] = None, username: Optional[str] = None) -> UserInfoDev:
173 | """
174 | 使用开发者接口获取用户数据,请确保拥有和输入了开发者 `token`
175 |
176 | Params:
177 | qqid: 用户QQ
178 | username: 查分器用户名
179 | Returns:
180 | `UserInfoDev` 开发者用户信息
181 | """
182 | params = {}
183 | if qqid:
184 | params['qq'] = qqid
185 | if username:
186 | params['username'] = username
187 |
188 | result = await self._requestmai('GET', '/dev/player/records', params=params)
189 | return UserInfoDev.model_validate(result)
190 |
191 | async def query_user_post_dev(
192 | self,
193 | *,
194 | qqid: Optional[int] = None,
195 | username: Optional[str] = None,
196 | music_id: Union[str, int, List[Union[str, int]]]
197 | ) -> List[PlayInfoDev]:
198 | """
199 | 使用开发者接口获取用户指定曲目数据,请确保拥有和输入了开发者 `token`
200 |
201 | Params:
202 | `qqid`: 用户QQ
203 | `username`: 查分器用户名
204 | `music_id`: 曲目id,可以为单个ID或者列表
205 | Returns:
206 | `List[PlayInfoDev]` 开发者成绩列表
207 | """
208 | json = {}
209 | if qqid:
210 | json['qq'] = qqid
211 | if username:
212 | json['username'] = username
213 | json['music_id'] = music_id
214 |
215 | result = await self._requestmai('POST', '/dev/player/record', json=json)
216 | if result == {}:
217 | raise MusicNotPlayError
218 |
219 | if isinstance(music_id, list):
220 | return [PlayInfoDev.model_validate(d) for k, v in result.items() for d in v]
221 | return [PlayInfoDev.model_validate(d) for d in result[str(music_id)]]
222 |
223 | async def rating_ranking(self) -> List[UserRanking]:
224 | """
225 | 获取查分器排行榜
226 |
227 | Returns:
228 | `List[UserRanking]` 按`ra`从高到低排序后的查分器排行模型列表
229 | """
230 | result = await self._requestmai('GET', '/rating_ranking')
231 | return sorted([UserRanking.model_validate(u) for u in result], key=lambda x: x.ra, reverse=True)
232 |
233 | async def get_plate_json(self) -> Dict[str, List[int]]:
234 | """获取所有版本牌子完成需求"""
235 | return await self._requestalias('GET', '/maimaidxplate')
236 |
237 | async def get_alias(self) -> Dict[str, Union[str, int, List[str]]]:
238 | """获取所有别名"""
239 | return await self._requestalias('GET', '/maimaidxalias')
240 |
241 | async def get_songs(self, name: str) -> Union[List[AliasStatus], List[Alias]]:
242 | """
243 | 使用别名查询曲目。
244 | 状态码为 `201` 时返回值为 `List[AliasStatus]`。
245 | 状态码为 `200` 时返回值为 `List[Alias]`。
246 |
247 | Params:
248 | `name`: 别名
249 | Returns:
250 | `Union[List[AliasStatus], List[Alias]]`
251 | """
252 | result = await self._requestalias('GET', '/getsongs', params={'name': name})
253 | if 'status_code' in result:
254 | r = [AliasStatus.model_validate(s) for s in result['content']]
255 | else:
256 | r = [Alias.model_validate(s) for s in result]
257 | return r
258 |
259 | async def get_songs_alias(self, song_id: int) -> Alias:
260 | """
261 | 使用曲目 `id` 查询别名
262 |
263 | Params:
264 | `song_id`: 曲目 `ID`
265 | Returns:
266 | `Alias`
267 | """
268 | result = await self._requestalias('GET', '/getsongsalias', params={'song_id': song_id})
269 | return Alias.model_validate(result)
270 |
271 | async def get_alias_status(self) -> List[AliasStatus]:
272 | """获取当前正在进行的别名投票"""
273 | result = await self._requestalias('GET', '/getaliasstatus')
274 | return [AliasStatus.model_validate(s) for s in result]
275 |
276 | async def post_alias(self, song_id: int, aliasname: str, user_id: int) -> AliasStatus:
277 | """
278 | 提交别名申请
279 |
280 | Params:
281 | `id`: 曲目 `id`
282 | `aliasname`: 别名
283 | `user_id`: 提交的用户
284 | Returns:
285 | `AliasStatus`
286 | """
287 | json = {
288 | 'SongID': song_id,
289 | 'ApplyAlias': aliasname,
290 | 'ApplyUID': user_id
291 | }
292 | return AliasStatus.model_validate(await self._requestalias('POST', '/applyalias', json=json))
293 |
294 | async def post_agree_user(self, tag: str, user_id: int) -> str:
295 | """
296 | 提交同意投票
297 |
298 | Params:
299 | `tag`: 标签
300 | `user_id`: 同意投票的用户
301 | Returns:
302 | `str`
303 | """
304 | json = {
305 | 'Tag': tag,
306 | 'AgreeUser': user_id
307 | }
308 | return await self._requestalias('POST', '/agreeuser', json=json)
309 |
310 | async def transfer_music(self):
311 | """中转查分器曲目数据"""
312 | return await self._requestalias('GET', '/maimaidxmusic')
313 |
314 | async def transfer_chart(self):
315 | """中转查分器单曲数据"""
316 | return await self._requestalias('GET', '/maimaidxchartstats')
317 |
318 | async def qqlogo(self, qqid: int = None, icon: str = None) -> Optional[bytes]:
319 | """获取QQ头像"""
320 | async with ClientSession(timeout=ClientTimeout(total=30)) as session:
321 | if qqid:
322 | params = {
323 | 'b': 'qq',
324 | 'nk': qqid,
325 | 's': 100
326 | }
327 | res = await session.request('GET', self.QQAPI, params=params)
328 | elif icon:
329 | res = await session.request('GET', icon)
330 | else:
331 | return None
332 | return await res.read()
333 |
334 |
335 | maiApi = MaimaiAPI()
--------------------------------------------------------------------------------
/libraries/maimaidx_arcade.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | import traceback
4 | from typing import Dict, List, Optional, Union
5 |
6 | import aiohttp
7 | from pydantic import BaseModel
8 |
9 | from .. import arcades_json, loga
10 | from .maimaidx_music import writefile
11 |
12 |
13 | class Arcade(BaseModel):
14 |
15 | name: str
16 | location: str
17 | province: str
18 | mall: str
19 | num: int
20 | id: str
21 | alias: List[str]
22 | group: List[int]
23 | person: int
24 | by: str
25 | time: str
26 |
27 |
28 | class ArcadeList(List[Arcade]):
29 |
30 |
31 | async def save_arcade(self):
32 | return await writefile(arcades_json, [_.model_dump() for _ in self])
33 |
34 | def search_name(self, name: str) -> List[Arcade]:
35 | """模糊查询机厅"""
36 | arcade_list = []
37 | for arcade in self:
38 | if name in arcade.name:
39 | arcade_list.append(arcade)
40 | elif name in arcade.location:
41 | arcade_list.append(arcade)
42 | elif name in arcade.alias:
43 | arcade_list.append(arcade)
44 |
45 | return arcade_list
46 |
47 | def search_fullname(self, name: str) -> List[Arcade]:
48 | """查询店铺全名机厅"""
49 | arcade_list = []
50 | for arcade in self:
51 | if name == arcade.name:
52 | arcade_list.append(arcade)
53 |
54 | return arcade_list
55 |
56 | def search_alias(self, alias: str) -> List[Arcade]:
57 | """查询别名机厅"""
58 | arcade_list = []
59 | for arcade in self:
60 | if alias in arcade.alias:
61 | arcade_list.append(arcade)
62 |
63 | return arcade_list
64 |
65 | def search_id(self, id: str) -> List[Arcade]:
66 | """指定ID查询机厅"""
67 | arcade_list = []
68 | for arcade in self:
69 | if id == arcade.id:
70 | arcade_list.append(arcade)
71 |
72 | return arcade_list
73 |
74 | def add_arcade(self, arcade: dict) -> bool:
75 | """添加机厅"""
76 | self.append(Arcade(**arcade))
77 | return True
78 |
79 | def del_arcade(self, arcadeName: str) -> bool:
80 | """删除机厅"""
81 | for arcade in self:
82 | if arcadeName == arcade.name:
83 | self.remove(arcade)
84 | return True
85 | return False
86 |
87 | def group_in_arcade(self, group_id: int, arcadeName: str) -> bool:
88 | """是否已订阅该机厅"""
89 | for arcade in self:
90 | if arcadeName == arcade.name:
91 | if group_id in arcade.group:
92 | return True
93 | return False
94 |
95 | def group_subscribe_arcade(self, group_id: int) -> List[Arcade]:
96 | """已订阅机厅"""
97 | arcade_list = []
98 | for arcade in self:
99 | if group_id in arcade.group:
100 | arcade_list.append(arcade)
101 | return arcade_list
102 |
103 | @classmethod
104 | def arcade_to_msg(cls, arcade_list: List[Arcade]) -> List[str]:
105 | """机厅人数格式化"""
106 | result = []
107 | for arcade in arcade_list:
108 | msg = f'''{arcade.name}
109 | - 当前 {arcade.person} 人\n'''
110 | if arcade.num > 1:
111 | msg += f' - 平均 {arcade.person / arcade.num:.2f} 人\n'
112 | if arcade.by:
113 | msg += f' - 由 {arcade.by} 更新于 {arcade.time}'
114 | result.append(msg.strip())
115 | return result
116 |
117 |
118 | class ArcadeData:
119 |
120 | total: Optional[ArcadeList]
121 |
122 | def __init__(self) -> None:
123 | self.arcades = []
124 | if arcades_json.exists():
125 | self.arcades: List[Dict] = json.load(open(arcades_json, 'r', encoding='utf-8'))
126 | self.idList = []
127 |
128 | def get_by_id(self, id: int) -> Optional[Dict]:
129 | id_list = [c_a['id'] for c_a in self.arcades]
130 | if id in id_list:
131 | return self.arcades[id_list.index(id)]
132 | else:
133 | return None
134 |
135 | async def getArcade(self):
136 | self.total = await download_arcade_info()
137 | self.idList = [int(c_a.id) for c_a in self.total]
138 |
139 | arcade = ArcadeData()
140 |
141 |
142 | async def download_arcade_info(save: bool = True) -> ArcadeList:
143 | try:
144 | async with aiohttp.request('GET', 'http://wc.wahlap.net/maidx/rest/location', timeout=aiohttp.ClientTimeout(total=30)) as req:
145 | if req.status == 200:
146 | data = await req.json()
147 | else:
148 | data = None
149 | loga.error('获取机厅信息失败')
150 | arcadelist = ArcadeList()
151 | if data is not None:
152 | if not arcade.arcades:
153 | for num in range(len(data)):
154 | _arc = data[num]
155 | arcade_dict = {
156 | 'name': _arc['arcadeName'],
157 | 'location': _arc['address'],
158 | 'province': _arc['province'],
159 | 'mall': _arc['mall'],
160 | 'num': _arc['machineCount'],
161 | 'id': _arc['id'],
162 | 'alias': [],
163 | 'group': [],
164 | 'person': 0,
165 | 'by': '',
166 | 'time': ''
167 | }
168 | arcadelist.append(Arcade.model_validate(arcade_dict))
169 | else:
170 | for num in range(len(data)):
171 | _arc = data[num]
172 | arcade_dict = arcade.get_by_id(_arc['id'])
173 | if arcade_dict is not None:
174 | arcade_dict['name'] = _arc['arcadeName']
175 | arcade_dict['location'] = _arc['address']
176 | arcade_dict['province'] = _arc['province']
177 | arcade_dict['mall'] = _arc['mall']
178 | arcade_dict['num'] = _arc['machineCount']
179 | arcade_dict['id'] = _arc['id']
180 | else:
181 | arcade_dict = {
182 | 'name': _arc['arcadeName'],
183 | 'location': _arc['address'],
184 | 'province': _arc['province'],
185 | 'mall': _arc['mall'],
186 | 'num': _arc['machineCount'],
187 | 'id': _arc['id'],
188 | 'alias': [],
189 | 'group': [],
190 | 'person': 0,
191 | 'by': '',
192 | 'time': ''
193 | }
194 | arcade.arcades.insert(num, arcade_dict)
195 | arcadelist.append(Arcade.model_validate(arcade_dict))
196 | for n in arcade.arcades:
197 | if int(n['id']) >= 10000:
198 | arcadelist.append(Arcade.model_validate(n))
199 | else:
200 | for _a in arcade.arcades:
201 | arcadelist.append(Arcade.model_validate(_a))
202 | if save:
203 | await writefile(arcades_json, [_.model_dump() for _ in arcadelist])
204 | return arcadelist
205 | except Exception:
206 | loga.error(f'Error: {traceback.format_exc()}')
207 | loga.error('获取机厅信息失败')
208 |
209 |
210 | async def updata_arcade(arcadeName: str, num: str):
211 | if arcadeName.isdigit():
212 | arcade_list = arcade.total.search_id(arcadeName)
213 | else:
214 | arcade_list = arcade.total.search_fullname(arcadeName)
215 | if arcade_list:
216 | _arcade = arcade_list[0]
217 | _arcade.num = int(num)
218 | msg = f'已修改机厅 [{arcadeName}] 机台数量为 [{num}]'
219 | await arcade.total.save_arcade()
220 | else:
221 | msg = f'未找到机厅:{arcadeName}'
222 | return msg
223 |
224 |
225 | async def update_alias(arcadeName: str, aliasName: str, add_del: bool):
226 | """变更机厅别名"""
227 | change = False
228 | if arcadeName.isdigit():
229 | arcade_list = arcade.total.search_id(arcadeName)
230 | else:
231 | arcade_list = arcade.total.search_fullname(arcadeName)
232 | if arcade_list:
233 | _arcade = arcade_list[0]
234 | if add_del:
235 | if aliasName not in _arcade.alias:
236 | _arcade.alias.append(aliasName)
237 | msg = f'机厅:{_arcade.name}\n已添加别名:{aliasName}'
238 | change = True
239 | else:
240 | msg = f'机厅:{_arcade.name}\n已拥有别名:{aliasName}\n请勿重复添加'
241 | else:
242 | if aliasName in _arcade.alias:
243 | _arcade.alias.remove(aliasName)
244 | msg = f'机厅:{_arcade.name}\n已删除别名:{aliasName}'
245 | change = True
246 | else:
247 | msg = f'机厅:{_arcade.name}\n未拥有别名:{aliasName}'
248 | else:
249 | msg = f'未找到机厅:{arcadeName}'
250 | if change:
251 | await arcade.total.save_arcade()
252 | return msg
253 |
254 |
255 | async def subscribe(group_id: int, arcadeName: str, sub: bool):
256 | """订阅机厅,`sub` 等于 `True` 为订阅,`False` 为取消订阅"""
257 | change = False
258 | if arcadeName.isdigit():
259 | arcade_list = arcade.total.search_id(arcadeName)
260 | else:
261 | arcade_list = arcade.total.search_fullname(arcadeName)
262 | if arcade_list:
263 | _arcade = arcade_list[0]
264 | if sub:
265 | if arcade.total.group_in_arcade(group_id, _arcade.name):
266 | msg = f'该群已订阅机厅:{_arcade.name}'
267 | else:
268 | _arcade.group.append(group_id)
269 | msg = f'群:{group_id} 已添加订阅机厅:{_arcade.name}'
270 | change = True
271 | else:
272 | if not arcade.total.group_in_arcade(group_id, _arcade.name):
273 | msg = f'该群未订阅机厅:{_arcade.name},无需取消订阅'
274 | else:
275 | _arcade.group.remove(group_id)
276 | msg = f'群:{group_id} 已取消订阅机厅:{_arcade.name}'
277 | change = True
278 | else:
279 | msg = f'未找到机厅:{arcadeName}'
280 | if change:
281 | await arcade.total.save_arcade()
282 | return msg
283 |
284 |
285 | async def update_person(arcadeList: List[Arcade], userName: str, value: str, person: int):
286 | """变更机厅人数"""
287 | if len(arcadeList) == 1:
288 | _arcade = arcadeList[0]
289 | original_person = _arcade.person
290 | if value in ['+', '+', '增加', '添加', '加']:
291 | if person > 30:
292 | return '请勿乱玩bot,恼!'
293 | _arcade.person += person
294 | elif value in ['-', '-', '减少', '降低', '减']:
295 | if person > 30 or person > _arcade.person:
296 | return '请勿乱玩bot,恼!'
297 | _arcade.person -= person
298 | elif value in ['=', '=', '设置', '设定']:
299 | if abs(_arcade.person - person) > 30:
300 | return '请勿乱玩bot,恼!'
301 | _arcade.person = person
302 | if _arcade.person == original_person:
303 | return f'人数没有变化\n机厅:{_arcade.name}\n当前人数:{_arcade.person}'
304 | else:
305 | _arcade.by = userName
306 | _arcade.time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
307 | await arcade.total.save_arcade()
308 | return f'机厅:{_arcade.name}\n当前人数:{_arcade.person}\n变更时间:{_arcade.time}'
309 | elif len(arcadeList) > 1:
310 | return '找到多个机厅,请使用id变更人数\n' + '\n'.join([f'{_.id}:{_.name}' for _ in arcadeList])
311 | else:
312 | return '没有找到指定机厅'
313 |
--------------------------------------------------------------------------------
/libraries/maimaidx_error.py:
--------------------------------------------------------------------------------
1 | from textwrap import dedent
2 |
3 |
4 | class UserNotFoundError(Exception):
5 |
6 | def __str__(self) -> str:
7 | return dedent('''
8 | 未找到此玩家,请确保此玩家的用户名和查分器中的用户名相同。
9 | 如未绑定,请前往查分器官网进行绑定
10 | https://www.diving-fish.com/maimaidx/prober/
11 | ''').strip()
12 |
13 |
14 | class UserNotExistsError(Exception):
15 |
16 | def __str__(self) -> str:
17 | return '查询的用户不存在'
18 |
19 |
20 | class UserDisabledQueryError(Exception):
21 |
22 | def __str__(self) -> str:
23 | return '该用户禁止了其他人获取数据或未同意用户协议。'
24 |
25 |
26 | class TokenError(Exception):
27 |
28 | def __str__(self) -> str:
29 | return '开发者Token有误'
30 |
31 |
32 | class TokenDisableError(Exception):
33 |
34 | def __str__(self) -> str:
35 | return '开发者Token被禁用'
36 |
37 |
38 | class TokenNotFoundError(Exception):
39 |
40 | def __str__(self) -> str:
41 | return '请先联系水鱼申请开发者token'
42 |
43 |
44 | class MusicNotPlayError(Exception):
45 |
46 | def __str__(self) -> str:
47 | return '您未游玩该曲目'
48 |
49 |
50 | class ServerError(Exception):
51 |
52 | def __str__(self) -> str:
53 | return '别名服务器错误,请联系插件开发者'
54 |
55 |
56 | class EnterError(Exception):
57 |
58 | def __str__(self) -> str:
59 | return '参数输入错误'
60 |
61 |
62 | class AliasesNotFoundError(Exception):
63 |
64 | def __str__(self) -> str:
65 | return '未找到别名'
66 |
67 |
68 | class UnknownError(Exception):
69 | """未知错误"""
--------------------------------------------------------------------------------
/libraries/maimaidx_model.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | from typing import List, Optional, Union
3 |
4 | from pydantic import BaseModel, Field
5 |
6 |
7 | ##### Music
8 | class Stats(BaseModel):
9 |
10 | cnt: Optional[float] = None
11 | diff: Optional[str] = None
12 | fit_diff: Optional[float] = None
13 | avg: Optional[float] = None
14 | avg_dx: Optional[float] = None
15 | std_dev: Optional[float] = None
16 | dist: Optional[List[int]] = None
17 | fc_dist: Optional[List[float]] = None
18 |
19 |
20 | Notes1 = namedtuple('Notes', ['tap', 'hold', 'slide', 'brk'])
21 | Notes2 = namedtuple('Notes', ['tap', 'hold', 'slide', 'touch', 'brk'])
22 |
23 |
24 | class Chart(BaseModel):
25 |
26 | notes: Union[Notes1, Notes2]
27 | charter: str = None
28 |
29 |
30 | class BasicInfo(BaseModel):
31 |
32 | title: str
33 | artist: str
34 | genre: str
35 | bpm: int
36 | release_date: Optional[str] = ''
37 | version: str = Field(alias='from')
38 | is_new: bool
39 |
40 |
41 | class Music(BaseModel):
42 |
43 | id: str
44 | title: str
45 | type: str
46 | ds: List[float]
47 | level: List[str]
48 | cids: List[int]
49 | charts: List[Chart]
50 | basic_info: BasicInfo
51 | stats: Optional[List[Optional[Stats]]] = []
52 | diff: Optional[List[int]] = []
53 |
54 |
55 | class RaMusic(BaseModel):
56 |
57 | id: str
58 | ds: float
59 | lv: str
60 | lvp: str
61 | type: str
62 |
63 |
64 | ##### Aliases
65 | class Alias(BaseModel):
66 |
67 | SongID: int
68 | Name: str
69 | Alias: List[str]
70 |
71 |
72 | class AliasStatus(BaseModel):
73 |
74 | Tag: str
75 | SongID: int
76 | ApplyAlias: str
77 | IsNew: bool
78 | IsEnd: bool
79 | Time: str
80 | AgreeVotes: int
81 | Votes: int
82 |
83 |
84 | ##### Guess
85 | class GuessData(BaseModel):
86 |
87 | music: Music
88 | img: str
89 | answer: List[str]
90 | end: bool = False
91 |
92 |
93 | class GuessDefaultData(GuessData):
94 |
95 | options: List[str]
96 |
97 |
98 | class GuessPicData(GuessData): ...
99 |
100 |
101 | class Switch(BaseModel):
102 |
103 | enable: List[int] = []
104 | disable: List[int] = []
105 |
106 |
107 | class GuessSwitch(Switch): ...
108 |
109 |
110 | ##### AliasesPush
111 | class AliasesPush(Switch):
112 |
113 | global_switch: bool = Field(True, alias='global')
114 |
115 |
116 | ##### Best50
117 | class PlayInfo(BaseModel):
118 |
119 | achievements: float
120 | fc: str = ''
121 | fs: str = ''
122 | level: str
123 | level_index: int
124 | title: str
125 | type: str
126 | ds: float = 0
127 | dxScore: int = 0
128 | ra: int = 0
129 | rate: str = ''
130 |
131 |
132 | class ChartInfo(PlayInfo):
133 |
134 | level_label: str
135 | song_id: int
136 |
137 |
138 | class Data(BaseModel):
139 |
140 | sd: Optional[List[ChartInfo]] = None
141 | dx: Optional[List[ChartInfo]] = None
142 |
143 |
144 | class _UserInfo(BaseModel):
145 |
146 | additional_rating: Optional[int]
147 | nickname: Optional[str]
148 | plate: Optional[str] = None
149 | rating: Optional[int]
150 | username: Optional[str]
151 |
152 |
153 | class UserInfo(_UserInfo):
154 |
155 | charts: Optional[Data]
156 |
157 | class PlayInfoDefault(PlayInfo):
158 |
159 | song_id: int = Field(alias='id')
160 | table_level: List[int] = []
161 |
162 |
163 | class PlayInfoDev(ChartInfo): ...
164 |
165 |
166 | class TableData(BaseModel):
167 |
168 | achievements: float
169 | fc: str = ''
170 |
171 |
172 | class PlanInfo(BaseModel):
173 |
174 | completed: Union[PlayInfoDefault, PlayInfoDev] = None
175 | unfinished: Union[PlayInfoDefault, PlayInfoDev] = None
176 |
177 |
178 | class RiseScore(BaseModel):
179 |
180 | song_id: int
181 | title: str
182 | type: str
183 | level_index: int
184 | ds: float
185 | ra: int
186 | rate: str
187 | achievements: float
188 | oldra: Optional[int] = 0
189 | oldrate: Optional[str] = 'D'
190 | oldachievements: Optional[float] = 0
191 |
192 |
193 | ##### Dev
194 | class UserInfoDev(_UserInfo):
195 |
196 | records: Optional[List[PlayInfoDev]] = None
197 |
198 |
199 | ##### Rank
200 | class UserRanking(BaseModel):
201 |
202 | username: str
203 | ra: int
--------------------------------------------------------------------------------
/libraries/maimaidx_music.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | import random
4 | import traceback
5 | from collections import Counter
6 | from copy import deepcopy
7 | from typing import Tuple
8 |
9 | import numpy as np
10 | from PIL import Image
11 |
12 | from .. import *
13 | from .image import image_to_base64, music_picture
14 | from .maimaidx_api_data import maiApi
15 | from .maimaidx_error import *
16 | from .maimaidx_model import *
17 | from .tool import openfile, writefile
18 |
19 |
20 | def cross(
21 | checker: Union[List[str], List[float]],
22 | elem: Optional[Union[str, float, List[str], List[float], Tuple[float, float]]],
23 | diff: List[int]
24 | ) -> Tuple[bool, List[int]]:
25 | ret = False
26 | diff_ret = []
27 | if not elem or elem is Ellipsis:
28 | return True, diff
29 | if isinstance(elem, List):
30 | for _j in (range(len(checker)) if diff is Ellipsis else diff):
31 | if _j >= len(checker):
32 | continue
33 | __e = checker[_j]
34 | if __e in elem:
35 | diff_ret.append(_j)
36 | ret = True
37 | elif isinstance(elem, Tuple):
38 | for _j in (range(len(checker)) if diff is Ellipsis else diff):
39 | if _j >= len(checker):
40 | continue
41 | __e = checker[_j]
42 | if elem[0] <= __e <= elem[1]:
43 | diff_ret.append(_j)
44 | ret = True
45 | else:
46 | for _j in (range(len(checker)) if diff is Ellipsis else diff):
47 | if _j >= len(checker):
48 | continue
49 | __e = checker[_j]
50 | if elem == __e:
51 | diff_ret.append(_j)
52 | ret = True
53 | return ret, diff_ret
54 |
55 |
56 | def in_or_equal(
57 | checker: Union[str, int],
58 | elem: Optional[Union[str, float, List[str], List[float], Tuple[float, float]]]
59 | ) -> bool:
60 | if elem is Ellipsis:
61 | return True
62 | if isinstance(elem, List):
63 | return checker in elem
64 | elif isinstance(elem, Tuple):
65 | return elem[0] <= checker <= elem[1]
66 | else:
67 | return checker == elem
68 |
69 |
70 | class MusicList(List[Music]):
71 |
72 | def by_id(self, music_id: Union[str, int]) -> Optional[Music]:
73 | for music in self:
74 | if music.id == str(music_id):
75 | return music
76 | return None
77 |
78 | def by_title(self, music_title: str) -> Optional[Music]:
79 | for music in self:
80 | if music.title == music_title:
81 | return music
82 | return None
83 |
84 | def by_plan(self, level: str) -> Dict[str, Union[PlanInfo, RaMusic, Dict[int, Union[PlanInfo, RaMusic]]]]:
85 | lv = {}
86 | for music in self:
87 | if level not in music.level:
88 | continue
89 | count = Counter(music.level)
90 | if count.get(level) > 1: # 同曲有相同等级
91 | lv[music.id] = {
92 | index: RaMusic(
93 | id=music.id,
94 | ds=music.ds[index],
95 | lv=str(index),
96 | lvp=music.level[index],
97 | type=music.type
98 | )
99 | for index, _lv in enumerate(music.level)
100 | if _lv == level
101 | }
102 | else:
103 | index = music.level.index(level)
104 | lv[music.id] = RaMusic(
105 | id=music.id,
106 | ds=music.ds[index],
107 | lv=str(index),
108 | lvp=music.level[index],
109 | type=music.type
110 | )
111 | return lv
112 |
113 | def by_level_list(self) -> Dict[str, Dict[str, List[RaMusic]]]:
114 | levellist = None
115 | _level: Dict[str, Dict[str, List[RaMusic]]] = {}
116 | for lv in levelList[5:]:
117 | if lv == '15':
118 | r = range(1)
119 | elif lv in levelList[:6]:
120 | r = range(9, -1, -1)
121 | elif '+' in lv:
122 | r = range(9, 6, -1)
123 | else:
124 | r = range(6, -1, -1)
125 | levellist = {f'{lv if "+" not in lv else lv[:-1]}.{_}': [] for _ in r}
126 | _level[lv] = levellist
127 | for music in self:
128 | if int(music.id) >= 100000:
129 | continue
130 | for index, ds in enumerate(music.ds):
131 | if ds < 6: continue
132 | ra = RaMusic(
133 | id=music.id,
134 | ds=ds,
135 | lv=str(index),
136 | lvp=music.level[index],
137 | type=music.type
138 | )
139 | _level[music.level[index]][str(ds)].append(ra)
140 | return _level
141 |
142 | def by_id_list(self, music_id_list: List[int]) -> Optional[List[Music]]:
143 | musicList = []
144 | for music in self:
145 | if int(music.id) in music_id_list:
146 | musicList.append(music)
147 | return musicList
148 |
149 | def random(self) -> Music:
150 | return random.choice(self)
151 |
152 | def filter(
153 | self,
154 | *,
155 | level: Optional[Union[str, List[str]]] = ...,
156 | ds: Optional[Union[float, List[float], Tuple[float, float]]] = ...,
157 | title_search: Optional[str] = ...,
158 | artist_search: Optional[str] = ...,
159 | charter_search: Optional[str] = ...,
160 | genre: Optional[Union[str, List[str]]] = ...,
161 | bpm: Optional[Union[float, List[float], Tuple[float, float]]] = ...,
162 | type: Optional[Union[str, List[str]]] = ...,
163 | diff: List[int] = ...,
164 | version: Union[str, List[str]] = ...
165 | ) -> 'MusicList':
166 | new_list = MusicList()
167 | for music in self:
168 | diff2 = diff
169 | music = deepcopy(music)
170 | ret, diff2 = cross(music.level, level, diff2)
171 | if not ret:
172 | continue
173 | ret, diff2 = cross(music.ds, ds, diff2)
174 | if not ret:
175 | continue
176 | ret, diff2 = search_charts(music.charts, charter_search, diff2)
177 | if not ret:
178 | continue
179 | if not in_or_equal(music.basic_info.genre, genre):
180 | continue
181 | if not in_or_equal(music.type, type):
182 | continue
183 | if not in_or_equal(music.basic_info.bpm, bpm):
184 | continue
185 | if not in_or_equal(music.basic_info.version, version):
186 | continue
187 | if title_search is not Ellipsis and title_search.lower() not in music.title.lower():
188 | continue
189 | if artist_search is not Ellipsis and artist_search.lower() not in music.basic_info.artist.lower():
190 | continue
191 | music.diff = diff2
192 | new_list.append(music)
193 | return new_list
194 |
195 |
196 | def search_charts(checker: List[Chart], elem: str, diff: List[int]) -> Tuple[bool, List[int]]:
197 | ret = False
198 | diff_ret = []
199 | if not elem or elem is Ellipsis:
200 | return True, diff
201 | for _j in (range(len(checker)) if diff is Ellipsis else diff):
202 | if elem.lower() in checker[_j].charter.lower():
203 | diff_ret.append(_j)
204 | ret = True
205 | return ret, diff_ret
206 |
207 |
208 | class AliasList(List[Alias]):
209 |
210 | def by_id(self, music_id: Union[str, int]) -> Optional[List[Alias]]:
211 | alias_music = []
212 | for music in self:
213 | if music.SongID == int(music_id):
214 | alias_music.append(music)
215 | return alias_music
216 |
217 | def by_alias(self, music_alias: str) -> Optional[List[Alias]]:
218 | alias_list = []
219 | for music in self:
220 | if music_alias in music.Alias:
221 | alias_list.append(music)
222 | return alias_list
223 |
224 |
225 | dataerror = dedent(f'''
226 | 未找到文件,请自行使用浏览器访问 "https://www.diving-fish.com/api/maimaidxprober/music_data"
227 | 将内容保存为 "music_data.json" 存放在 "static" 目录下并重启bot
228 | ''').strip()
229 | charterror = dedent(f'''
230 | 未找到文件,请自行使用浏览器访问 "https://www.diving-fish.com/api/maimaidxprober/chart_stats"
231 | 将内容保存为 "music_chart.json" 存放在 "static" 目录下并重启bot
232 | ''').strip()
233 | aliaserror = dedent('''
234 | 本地暂存别名文件为空,请自行使用浏览器访问 "https://www.yuzuchan.moe/api/maimaidx/maimaidxalias"
235 | 获取别名数据并保存在 "static/music_alias.json" 文件中并重启bot
236 | ''').strip()
237 |
238 |
239 | async def get_music_list() -> MusicList:
240 | """获取所有数据"""
241 | # MusicData
242 | try:
243 | try:
244 | music_data = await maiApi.music_data()
245 | await writefile(music_file, music_data)
246 | except asyncio.exceptions.TimeoutError:
247 | log.error('从diving-fish获取maimaiDX曲库数据超时,正在使用yuzuapi中转获取曲库数据')
248 | try:
249 | music_data = await maiApi.transfer_music()
250 | await writefile(music_file, music_data)
251 | except ServerError:
252 | log.error('从yuzuapi获取maimaiDX曲库数据失败,请检查网络环境。已切换至本地暂存文件')
253 | music_data = await openfile(music_file)
254 | except Exception:
255 | log.error(f'Error: {traceback.format_exc()}')
256 | log.error('maimaiDX曲库数据获取失败,请检查网络环境。已切换至本地暂存文件')
257 | music_data = await openfile(music_file)
258 | except FileNotFoundError:
259 | log.error(dataerror)
260 | raise FileNotFoundError
261 |
262 | # ChartStats
263 | try:
264 | try:
265 | chart_stats = await maiApi.chart_stats()
266 | await writefile(chart_file, chart_stats)
267 | except asyncio.exceptions.TimeoutError:
268 | log.error('从diving-fish获取maimaiDX数据获取超时,正在使用yuzuapi中转获取单曲数据')
269 | try:
270 | chart_stats = await maiApi.transfer_chart()
271 | await writefile(chart_file, chart_stats)
272 | except ServerError:
273 | log.error('从yuzuapi获取maimaiDX单曲数据获取错误,已切换至本地暂存文件')
274 | chart_stats = await openfile(chart_file)
275 | except Exception:
276 | log.error(f'Error: {traceback.format_exc()}')
277 | log.error('maimaiDX数据获取错误,请检查网络环境,已切换至本地暂存文件')
278 | chart_stats = await openfile(chart_file)
279 | except FileNotFoundError:
280 | log.error(charterror)
281 | raise FileNotFoundError
282 |
283 | total_list = MusicList()
284 | for music in music_data:
285 | if music['id'] in chart_stats['charts']:
286 | _stats = [
287 | _data if _data else None
288 | for _data in chart_stats['charts'][music['id']]
289 | ] if {} in chart_stats['charts'][music['id']] else \
290 | chart_stats['charts'][music['id']]
291 | else:
292 | _stats = None
293 | for lv in ['5+', '6+']:
294 | if lv in music['level']:
295 | errorlv = music['level'].index(lv)
296 | music['level'][errorlv] = lv[:-1]
297 | total_list.append(Music(stats=_stats, **music))
298 |
299 | return total_list
300 |
301 |
302 | async def get_music_alias_list() -> AliasList:
303 | """获取所有别名"""
304 | if local_alias_file.exists():
305 | local_alias_data = await openfile(local_alias_file)
306 | else:
307 | local_alias_data = {}
308 | alias_data: List[Dict[str, Union[int, str, List[str]]]] = []
309 | try:
310 | alias_data = await maiApi.get_alias()
311 | await writefile(alias_file, alias_data)
312 | except asyncio.exceptions.TimeoutError:
313 | log.error('获取别名超时。已切换至本地暂存文件')
314 | alias_data = await openfile(alias_file)
315 | if not alias_data:
316 | log.error(aliaserror)
317 | raise ValueError
318 | except ServerError as e:
319 | log.error(e)
320 | alias_data = await openfile(alias_file)
321 | except UnknownError:
322 | log.error('获取所有曲目别名信息错误,请检查网络环境。已切换至本地暂存文件')
323 | alias_data = await openfile(alias_file)
324 | if not alias_data:
325 | log.error(aliaserror)
326 | raise ValueError
327 |
328 | total_alias_list = AliasList()
329 | for _a in filter(lambda x: mai.total_list.by_id(x['SongID']), alias_data):
330 | if (song_id := str(_a['SongID'])) in local_alias_data:
331 | _a['Alias'].extend(local_alias_data[song_id])
332 | total_alias_list.append(Alias(**_a))
333 |
334 | return total_alias_list
335 |
336 |
337 | async def update_local_alias(id: str, alias_name: str) -> bool:
338 | try:
339 | if local_alias_file.exists():
340 | local_alias_data: Dict[str, List[str]] = await openfile(local_alias_file)
341 | else:
342 | local_alias_data: Dict[str, List[str]] = {}
343 | if id not in local_alias_data:
344 | local_alias_data[id] = []
345 |
346 | local_alias_data[id].append(alias_name.lower())
347 | mai.total_alias_list.by_id(id)[0].Alias.append(alias_name.lower())
348 | await writefile(local_alias_file, local_alias_data)
349 | return True
350 | except Exception as e:
351 | log.error(f'添加本地别名失败: {e}')
352 | return False
353 |
354 |
355 | class MaiMusic:
356 |
357 | total_list: MusicList
358 | """曲目数据"""
359 | total_alias_list: AliasList
360 | """别名数据"""
361 | total_plate_id_list: Dict[str, List[int]]
362 | """牌子ID列表数据"""
363 | total_level_data: Dict[str, Dict[str, List[RaMusic]]]
364 | """等级列表数据"""
365 | hot_music_ids: List = []
366 | """游玩次数超过1w次的曲目数据"""
367 | guess_data: List[Music]
368 | """猜歌数据"""
369 |
370 | def __init__(self) -> None:
371 | """封装所有曲目信息以及猜歌数据,便于更新"""
372 |
373 | async def get_music(self) -> None:
374 | """获取所有曲目数据"""
375 | self.total_list = await get_music_list()
376 | self.total_level_data = self.total_list.by_level_list()
377 |
378 | async def get_music_alias(self) -> None:
379 | """获取所有曲目别名"""
380 | self.total_alias_list = await get_music_alias_list()
381 |
382 | async def get_plate_json(self) -> None:
383 | """获取所有牌子数据"""
384 | self.total_plate_id_list = await maiApi.get_plate_json()
385 |
386 | def guess(self):
387 | """初始化猜歌数据"""
388 | for music in self.total_list:
389 | if music.stats:
390 | count = 0
391 | for stats in music.stats:
392 | if stats:
393 | count += stats.cnt if stats.cnt else 0
394 | if count > 10000:
395 | self.hot_music_ids.append(music.id)
396 | self.guess_data = list(filter(lambda x: x.id in self.hot_music_ids, self.total_list))
397 |
398 |
399 | mai = MaiMusic()
400 |
401 |
402 | class Guess:
403 |
404 | Group: Dict[int, Union[GuessDefaultData, GuessPicData]] = {}
405 | switch: GuessSwitch
406 |
407 | def __init__(self) -> None:
408 | """猜歌类"""
409 | if not guess_file.exists():
410 | self.switch = GuessSwitch()
411 | else:
412 | self.switch = GuessSwitch.model_validate(
413 | json.load(open(guess_file, 'r', encoding='utf-8'))
414 | )
415 |
416 | def start(self, gid: int):
417 | """开始猜歌"""
418 | self.Group[gid] = self.guessData()
419 |
420 | def startpic(self, gid: int):
421 | """开始猜曲绘"""
422 | self.Group[gid] = self.guesspicdata()
423 |
424 | def calculate_frequency_weights(self, image: Image.Image) -> np.ndarray:
425 | """
426 | 计算图像的频率权重,用于在图像中选择裁剪区域
427 |
428 | Params:
429 | `image`: PIL.Image.Image, 输入图像
430 | Returns:
431 | `np.ndarray` 频率权重矩阵
432 | """
433 | gray_image = np.array(image.convert('L'))
434 | freq = np.fft.fft2(gray_image)
435 | freq_shift = np.fft.fftshift(freq)
436 | magnitude = np.abs(freq_shift)
437 | normalized_magnitude = magnitude / magnitude.max()
438 | weights = normalized_magnitude ** 2
439 | return weights
440 |
441 | def select_crop_region(
442 | self,
443 | weights: np.ndarray,
444 | crop_width: int,
445 | crop_height: int,
446 | top_p: int
447 | ) -> Tuple[int, int]:
448 | h, w = weights.shape
449 | valid_regions = weights[:h - crop_height + 1, :w - crop_width + 1]
450 | flattened_weights = valid_regions.flatten()
451 | threshold = np.percentile(flattened_weights, top_p)
452 | valid_indices = np.where(flattened_weights >= threshold)[0]
453 | probabilities = flattened_weights[valid_indices]
454 | probabilities /= probabilities.sum()
455 | chosen_index = np.random.choice(valid_indices, p=probabilities)
456 | top_left_y = chosen_index // valid_regions.shape[1]
457 | top_left_x = chosen_index % valid_regions.shape[1]
458 | return top_left_x, top_left_y
459 |
460 | def pic(self, music: Music) -> Image.Image:
461 | """裁切曲绘"""
462 | im = Image.open(music_picture(music.id))
463 | w, h = im.size
464 | weights = self.calculate_frequency_weights(im)
465 | scale = random.uniform(0.15, 0.4) # 裁剪尺寸范围 可在此修改
466 | w2, h2 = int(w * scale), int(h * scale)
467 | top_p = min(1.3 - np.power(scale, 0.4), 0.95) * 100
468 | x, y = self.select_crop_region(weights, w2, h2, top_p)
469 | im = im.crop((x, y, x + w2, y + h2))
470 | return im
471 |
472 | def guesspicdata(self) -> GuessPicData:
473 | """猜曲绘数据"""
474 | music = random.choice(mai.guess_data)
475 | pic = self.pic(music)
476 | answer = mai.total_alias_list.by_id(music.id)[0].Alias
477 | answer.append(music.id)
478 | return GuessPicData(music=music, img=image_to_base64(pic), answer=answer, end=False)
479 |
480 | def guessData(self) -> GuessDefaultData:
481 | """猜歌数据"""
482 | music = random.choice(mai.guess_data)
483 | guess_options = random.sample([
484 | f'的 Expert 难度是 {music.level[2]}',
485 | f'的 Master 难度是 {music.level[3]}',
486 | f'的分类是 {music.basic_info.genre}',
487 | f'的版本是 {music.basic_info.version}',
488 | f'的艺术家是 {music.basic_info.artist}',
489 | f'{"不" if music.type == "SD" else ""}是 DX 谱面',
490 | f'{"没" if len(music.ds) == 4 else ""}有白谱',
491 | f'的 BPM 是 {music.basic_info.bpm}'
492 | ], 6)
493 | answer = mai.total_alias_list.by_id(music.id)[0].Alias
494 | answer.append(music.id)
495 | pic = self.pic(music)
496 | return GuessDefaultData(
497 | music=music,
498 | img=image_to_base64(pic),
499 | answer=answer,
500 | end=False,
501 | options=guess_options
502 | )
503 |
504 | def end(self, gid: str):
505 | """结束猜歌"""
506 | del self.Group[gid]
507 |
508 | async def on(self, gid: str) -> str:
509 | """开启猜歌"""
510 | if gid not in self.switch.enable:
511 | self.switch.enable.append(gid)
512 | if gid in self.switch.disable:
513 | self.switch.disable.remove(gid)
514 | await writefile(guess_file, self.switch.model_dump())
515 | return '群猜歌功能已开启'
516 |
517 | async def off(self, gid: str) -> str:
518 | """关闭猜歌"""
519 | if gid not in self.switch.disable:
520 | self.switch.disable.append(gid)
521 | if gid in self.switch.enable:
522 | self.switch.enable.remove(gid)
523 | if gid in self.Group:
524 | self.end(gid)
525 | await writefile(guess_file, self.switch.model_dump())
526 | return '群猜歌功能已关闭'
527 |
528 |
529 | guess = Guess()
530 |
531 |
532 | class GroupAlias:
533 |
534 | push: AliasesPush
535 |
536 | def __init__(self) -> None:
537 | """别名推送类"""
538 | if not group_alias_file.exists():
539 | self.push = AliasesPush()
540 | else:
541 | self.push = AliasesPush.model_validate(
542 | json.load(open(group_alias_file, 'r', encoding='utf-8'))
543 | )
544 |
545 | async def on(self, gid: int) -> str:
546 | """开启推送"""
547 | if gid not in self.push.enable:
548 | self.push.enable.append(gid)
549 | if gid in self.push.disable:
550 | self.push.disable.remove(gid)
551 | await writefile(group_alias_file, self.push.model_dump(by_alias=True))
552 | return '群别名推送功能已开启'
553 |
554 | async def off(self, gid: int) -> str:
555 | """关闭推送"""
556 | if gid not in self.push.disable:
557 | self.push.disable.append(gid)
558 | if gid in self.push.enable:
559 | self.push.enable.remove(gid)
560 | await writefile(group_alias_file, self.push.model_dump(by_alias=True))
561 | return '群别名推送功能已关闭'
562 |
563 | async def alias_global_change(self, set: bool):
564 | """修改全局开关"""
565 | self.push.global_switch = set
566 | await writefile(group_alias_file, self.push.model_dump(by_alias=True))
567 |
568 |
569 | alias = GroupAlias()
570 |
--------------------------------------------------------------------------------
/libraries/maimaidx_music_info.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from .image import rounded_corners
4 | from .maimai_best_50 import *
5 | from .maimaidx_music import Music, mai
6 |
7 |
8 | def newbestscore(song_id: str, lv: int, value: int, bestlist: List[ChartInfo]) -> int:
9 | for v in bestlist:
10 | if song_id == str(v.song_id) and lv == v.level_index:
11 | if value >= v.ra:
12 | return value - v.ra
13 | else:
14 | return 0
15 | return value - bestlist[-1].ra
16 |
17 |
18 | async def draw_music_info(music: Music, qqid: Optional[int] = None, user: Optional[UserInfo] = None) -> MessageSegment:
19 | """
20 | 查看谱面
21 |
22 | Params:
23 | `music`: 曲目模型
24 | `qqid`: QQID
25 | `user`: 用户模型
26 | Returns:
27 | `MessageSegment`
28 | """
29 | calc = True
30 | isfull = True
31 | bestlist: List[ChartInfo] = []
32 | try:
33 | if qqid:
34 | if user is None:
35 | player = await maiApi.query_user_b50(qqid=qqid)
36 | else:
37 | player = user
38 | if music.basic_info.version in list(plate_to_version.values())[-2]:
39 | bestlist = player.charts.dx
40 | isfull = bool(len(bestlist) == 15)
41 | else:
42 | bestlist = player.charts.sd
43 | isfull = bool(len(bestlist) == 35)
44 | else:
45 | calc = False
46 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError):
47 | calc = False
48 | except Exception:
49 | calc = False
50 |
51 | im = Image.open(maimaidir / 'song_bg.png').convert('RGBA')
52 | dr = ImageDraw.Draw(im)
53 | mr = DrawText(dr, SIYUAN)
54 | tb = DrawText(dr, TBFONT)
55 |
56 | default_color = (124, 130, 255, 255)
57 |
58 | im.alpha_composite(Image.open(maimaidir / 'logo.png').resize((249, 120)), (65, 25))
59 | if music.basic_info.is_new:
60 | im.alpha_composite(Image.open(maimaidir / 'UI_CMN_TabTitle_NewSong.png').resize((249, 120)), (940, 100))
61 | songbg = Image.open(music_picture(music.id)).resize((280, 280))
62 | im.alpha_composite(rounded_corners(songbg, 17, (True, False, False, True)), (110, 180))
63 | im.alpha_composite(Image.open(maimaidir / f'{music.basic_info.version}.png').resize((182, 90)), (800, 370))
64 | im.alpha_composite(Image.open(maimaidir / f'{music.type}.png').resize((80, 30)), (410, 375))
65 |
66 | title = music.title
67 | if coloumWidth(title) > 40:
68 | title = changeColumnWidth(title, 39) + '...'
69 | mr.draw(405, 220, 28, title, default_color, 'lm')
70 | artist = music.basic_info.artist
71 | if coloumWidth(artist) > 50:
72 | artist = changeColumnWidth(artist, 49) + '...'
73 | mr.draw(407, 265, 20, artist, default_color, 'lm')
74 | tb.draw(460, 330, 30, music.basic_info.bpm, default_color, 'lm')
75 | tb.draw(405, 435, 28, f'ID {music.id}', default_color, 'lm')
76 | mr.draw(665, 435, 24, music.basic_info.genre, default_color, 'mm')
77 |
78 | for num, _ in enumerate(music.level):
79 | if num == 4:
80 | color = (255, 255, 255, 255)
81 | else:
82 | color = (255, 255, 255, 255)
83 | tb.draw(181, 610 + 73 * num, 30, f'{music.level[num]}({music.ds[num]})', color, 'mm')
84 | tb.draw(
85 | 315, 600 + 73 * num, 30,
86 | f'{round(music.stats[num].fit_diff, 2):.2f}' if music.stats and music.stats[num] else '-',
87 | default_color, 'mm'
88 | )
89 | notes = list(music.charts[num].notes)
90 | tb.draw(437, 600 + 73 * num, 30, sum(notes), default_color, 'mm')
91 | if len(notes) == 4:
92 | notes.insert(3, '-')
93 | for n, c in enumerate(notes):
94 | tb.draw(556 + 119 * n, 600 + 73 * num, 30, c, default_color, 'mm')
95 | if num > 1:
96 | charter = music.charts[num].charter
97 | if coloumWidth(charter) > 19:
98 | charter = changeColumnWidth(charter, 18) + '...'
99 | mr.draw(372, 1030 + 47 * (num - 2), 18, charter, default_color, 'mm')
100 | ra = sorted([computeRa(music.ds[num], r) for r in achievementList[-6:]], reverse=True)
101 | for _n, value in enumerate(ra):
102 | size = 25
103 | if not calc:
104 | rating = value
105 | elif not isfull:
106 | size = 20
107 | rating = f'{value}(+{value})'
108 | elif value > bestlist[-1].ra:
109 | new = newbestscore(music.id, num, value, bestlist)
110 | if new == 0:
111 | rating = value
112 | else:
113 | size = 20
114 | rating = f'{value}(+{new})'
115 | else:
116 | rating = value
117 | tb.draw(536 + 101 * _n, 1030 + 47 * (num - 2), size, rating, default_color, 'mm')
118 | mr.draw(600, 1212, 22, f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} BOT', default_color, 'mm')
119 | return MessageSegment.image(image_to_base64(im))
120 |
121 |
122 | async def draw_music_play_data(qqid: int, music_id: str) -> Union[str, MessageSegment]:
123 | """
124 | 谱面游玩
125 |
126 | Params:
127 | `qqid`: QQID
128 | `music_id`: 曲目ID
129 | Returns:
130 | `Union[str, MessageSegment]`
131 | """
132 | try:
133 | diff: List[Union[None, PlayInfoDev, PlayInfoDefault]]
134 | if maiApi.token:
135 | data = await maiApi.query_user_post_dev(qqid=qqid, music_id=music_id)
136 | if not data:
137 | raise MusicNotPlayError
138 |
139 | music = mai.total_list.by_id(music_id)
140 | diff = [None for _ in music.ds]
141 | for _d in data:
142 | diff[_d.level_index] = _d
143 | dev = True
144 | else:
145 | version = list(set(_v for _v in plate_to_version.values()))
146 | data = await maiApi.query_user_plate(qqid=qqid, version=version)
147 |
148 | music = mai.total_list.by_id(music_id)
149 | _temp = [None for _ in music.ds]
150 | diff = copy.deepcopy(_temp)
151 |
152 | for _d in data:
153 | if _d.song_id == int(music_id):
154 | diff[_d.level_index] = _d
155 | if diff == _temp:
156 | raise MusicNotPlayError
157 | dev = False
158 |
159 | im = Image.open(maimaidir / 'info_bg.png').convert('RGBA')
160 |
161 | dr = ImageDraw.Draw(im)
162 | tb = DrawText(dr, TBFONT)
163 | mr = DrawText(dr, SIYUAN)
164 |
165 | im.alpha_composite(Image.open(maimaidir / 'logo.png').resize((249, 120)), (0, 34))
166 | cover = Image.open(music_picture(music_id))
167 | im.alpha_composite(cover.resize((300, 300)), (100, 260))
168 | im.alpha_composite(Image.open(maimaidir / f'info-{category[music.basic_info.genre]}.png'), (100, 260))
169 | im.alpha_composite(Image.open(maimaidir / f'{music.basic_info.version}.png').resize((183, 90)), (295, 205))
170 | im.alpha_composite(Image.open(maimaidir / f'{music.type}.png').resize((55, 20)), (350, 560))
171 |
172 | color = (124, 129, 255, 255)
173 | artist = music.basic_info.artist
174 | if coloumWidth(artist) > 58:
175 | artist = changeColumnWidth(artist, 57) + '...'
176 | mr.draw(255, 595, 12, artist, color, 'mm')
177 | title = music.title
178 | if coloumWidth(title) > 38:
179 | title = changeColumnWidth(title, 37) + '...'
180 | mr.draw(255, 622, 18, title, color, 'mm')
181 | tb.draw(160, 720, 22, music.id, color, 'mm')
182 | tb.draw(380, 720, 22, music.basic_info.bpm, color, 'mm')
183 |
184 | y = 100
185 | for num, info in enumerate(diff):
186 | im.alpha_composite(Image.open(maimaidir / f'd-{num}.png'), (650, 235 + y * num))
187 | if info:
188 | im.alpha_composite(Image.open(maimaidir / 'ra-dx.png'), (850, 272 + y * num))
189 | if dev:
190 | dxscore = info.dxScore
191 | _dxscore = sum(music.charts[num].notes) * 3
192 | dxnum = dxScore(dxscore / _dxscore * 100)
193 | rating, rate = info.ra, score_Rank_l[info.rate]
194 | if dxnum != 0:
195 | im.alpha_composite(
196 | Image.open(maimaidir / f'UI_GAM_Gauge_DXScoreIcon_0{dxnum}.png').resize((32, 19)),
197 | (851, 296 + y * num)
198 | )
199 | tb.draw(916, 304 + y * num, 13, f'{dxscore}/{_dxscore}', color, 'mm')
200 | else:
201 | rating, rate = computeRa(music.ds[num], info.achievements, israte=True)
202 |
203 | im.alpha_composite(Image.open(maimaidir / 'fcfs.png'), (965, 265 + y * num))
204 | if info.fc:
205 | im.alpha_composite(
206 | Image.open(maimaidir / f'UI_CHR_PlayBonus_{fcl[info.fc]}.png').resize((65, 65)),
207 | (960, 261 + y * num)
208 | )
209 | if info.fs:
210 | im.alpha_composite(
211 | Image.open(maimaidir / f'UI_CHR_PlayBonus_{fsl[info.fs]}.png').resize((65, 65)),
212 | (1025, 261 + y * num)
213 | )
214 | im.alpha_composite(Image.open(maimaidir / 'ra.png'), (1350, 405 + y * num))
215 | im.alpha_composite(
216 | Image.open(maimaidir / f'UI_TTR_Rank_{rate}.png').resize((100, 45)),
217 | (737, 272 + y * num)
218 | )
219 |
220 | tb.draw(510, 292 + y * num, 42, f'{info.achievements:.4f}%', color, 'lm')
221 | tb.draw(685, 248 + y * num, 25, music.ds[num], anchor='mm')
222 | tb.draw(915, 283 + y * num, 18, rating, color, 'mm')
223 | else:
224 | tb.draw(685, 248 + y * num, 25, music.ds[num], anchor='mm')
225 | mr.draw(800, 302 + y * num, 30, '未游玩', color, 'mm')
226 | if len(diff) == 4:
227 | mr.draw(800, 302 + y * 4, 30, '没有该难度', color, 'mm')
228 |
229 | mr.draw(600, 827, 22, f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} Bot', color, 'mm')
230 | msg = MessageSegment.image(image_to_base64(im))
231 |
232 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError, MusicNotPlayError) as e:
233 | msg = str(e)
234 | except Exception as e:
235 | log.error(traceback.format_exc())
236 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
237 | return msg
238 |
239 |
240 | def calc_achievements_fc(scorelist: Union[List[float], List[str]], lvlist_num: int, isfc: bool = False) -> int:
241 | r = -1
242 | obj = range(4) if isfc else achievementList[-6:]
243 | for __f in obj:
244 | if len(list(filter(lambda x: x >= __f, scorelist))) == lvlist_num:
245 | r += 1
246 | else:
247 | break
248 | return r
249 |
250 |
251 | def draw_rating(rating: str, path: Path) -> MessageSegment:
252 | """
253 | 绘制指定定数表文字
254 |
255 | Params:
256 | `rating`: 定数
257 | `path`: 路径
258 | Returns:
259 | `MessageSegment`
260 | """
261 | im = Image.open(path)
262 | dr = ImageDraw.Draw(im)
263 | sy = DrawText(dr, SIYUAN)
264 | sy.draw(700, 100, 65, f'Level.{rating} 定数表', (124, 129, 255, 255), 'mm', 5, (255, 255, 255, 255))
265 | return MessageSegment.image(image_to_base64(im))
266 |
267 |
268 | async def draw_rating_table(qqid: int, rating: str, isfc: bool = False) -> Union[MessageSegment, str]:
269 | """绘制定数表"""
270 | try:
271 | version = list(set(_v for _v in plate_to_version.values()))
272 | obj = await maiApi.query_user_plate(qqid=qqid, version=version)
273 |
274 | statistics = {
275 | 'clear': 0,
276 | 'sync': 0,
277 | 's': 0,
278 | 'sp': 0,
279 | 'ss': 0,
280 | 'ssp': 0,
281 | 'sss': 0,
282 | 'sssp': 0,
283 | 'fc': 0,
284 | 'fcp': 0,
285 | 'ap': 0,
286 | 'app': 0,
287 | 'fs': 0,
288 | 'fsp': 0,
289 | 'fsd': 0,
290 | 'fsdp': 0,
291 | }
292 | fromid = {}
293 |
294 | sp = score_Rank[-6:]
295 | for _d in obj:
296 | if _d.level != rating:
297 | continue
298 | if (id := str(_d.song_id)) not in fromid:
299 | fromid[id] = {}
300 | fromid[id][str(_d.level_index)] = {
301 | 'achievements': _d.achievements,
302 | 'fc': _d.fc,
303 | 'level': _d.level
304 | }
305 | rate = computeRa(_d.ds, _d.achievements, onlyrate=True).lower()
306 | if _d.achievements >= 80:
307 | statistics['clear'] += 1
308 | if rate in sp:
309 | r_index = sp.index(rate)
310 | for _r in range(r_index + 1):
311 | statistics[sp[_r]] += 1
312 | if _d.fc:
313 | fc_index = combo_rank.index(_d.fc)
314 | for _f in range(fc_index + 1):
315 | statistics[combo_rank[_f]] += 1
316 | if _d.fs:
317 | if _d.fs == 'sync':
318 | statistics[_d.fs] += 1
319 | else:
320 | fs_index = sync_rank.index(_d.fs)
321 | for _s in range(fs_index + 1):
322 | statistics[sync_rank[_s]] += 1
323 |
324 | achievements_fc_list: List[Union[float, List[float]]] = []
325 | lvlist = mai.total_level_data[rating]
326 | lvnum = sum([len(v) for v in lvlist.values()])
327 |
328 | rating_bg = Image.open(maimaidir / 'rating_bg.png')
329 | unfinished_bg = Image.open(maimaidir / 'unfinished_bg.png')
330 | complete_bg = Image.open(maimaidir / 'complete_bg.png')
331 |
332 | bg = ratingdir / f'{rating}.png'
333 |
334 | im = Image.open(bg).convert('RGBA')
335 | dr = ImageDraw.Draw(im)
336 | sy = DrawText(dr, SIYUAN)
337 | tb = DrawText(dr, TBFONT)
338 |
339 | im.alpha_composite(rating_bg, (600, 25))
340 | sy.draw(305, 60, 65, f'Level.{rating}', (124, 129, 255, 255), 'mm', 5, (255, 255, 255, 255))
341 | sy.draw(305, 130, 65, '定数表', (124, 129, 255, 255), 'mm', 5, (255, 255, 255, 255))
342 | tb.draw(700, 127, 45, lvnum, (124, 129, 255, 255), 'mm', 5, (255, 255, 255, 255))
343 |
344 | y = 22
345 | for n, v in enumerate(statistics):
346 | if n % 8 == 0:
347 | x = 824
348 | y += 56
349 | else:
350 | x += 64
351 | tb.draw(x, y, 20, statistics[v], (124, 129, 255, 255), 'mm', 2, (255, 255, 255, 255))
352 |
353 | y = 118
354 | for ra in lvlist:
355 | x = 158
356 | y += 20
357 | for num, music in enumerate(lvlist[ra]):
358 | if num % 14 == 0:
359 | x = 158
360 | y += 85
361 | else:
362 | x += 85
363 | if music.id in fromid and music.lv in fromid[music.id]:
364 | if not isfc:
365 | score = fromid[music.id][music.lv]['achievements']
366 | achievements_fc_list.append(score)
367 | rate = computeRa(music.ds, score, onlyrate=True)
368 | rank = Image.open(maimaidir / f'UI_TTR_Rank_{rate}.png').resize((78, 35))
369 | if score >= 100:
370 | im.alpha_composite(complete_bg, (x + 2, y - 18))
371 | else:
372 | im.alpha_composite(unfinished_bg, (x + 2, y - 18))
373 | im.alpha_composite(rank, (x, y - 5))
374 | continue
375 | if _fc := fromid[music.id][music.lv]['fc']:
376 | achievements_fc_list.append(combo_rank.index(_fc))
377 | fc = Image.open(maimaidir / f'UI_MSS_MBase_Icon_{fcl[_fc]}.png').resize((50, 50))
378 | im.alpha_composite(complete_bg, (x + 2, y - 18))
379 | im.alpha_composite(fc, (x + 15, y - 12))
380 |
381 | if len(achievements_fc_list) == lvnum:
382 | r = calc_achievements_fc(achievements_fc_list, lvnum, isfc)
383 | if r != -1:
384 | pic = fcl[combo_rank[r]] if isfc else score_Rank_l[score_Rank[-6:][r]]
385 | im.alpha_composite(Image.open(maimaidir / f'UI_MSS_Allclear_Icon_{pic}.png'), (40, 40))
386 |
387 | msg = MessageSegment.image(image_to_base64(im))
388 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
389 | msg = str(e)
390 | except Exception as e:
391 | log.error(traceback.format_exc())
392 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
393 | return msg
394 |
395 |
396 | async def draw_plate_table(qqid: int, version: str, plan: str) -> Union[MessageSegment, str]:
397 | """
398 | 绘制完成表
399 |
400 | Params:
401 | `qqid`: QQID
402 | `version`: 版本
403 | `plan`: 计划
404 | Returns:
405 | `Union[MessageSegment, str]`
406 | """
407 | try:
408 | if version in platecn:
409 | version = platecn[version]
410 | if version == '真':
411 | ver = [plate_to_version['真']] + [plate_to_version['初']]
412 | _ver = version
413 | elif version in ['熊', '华', '華']:
414 | ver = [plate_to_version['熊']]
415 | _ver = '熊&华'
416 | elif version in ['爽', '煌']:
417 | ver = [plate_to_version['爽']]
418 | _ver = '爽&煌'
419 | elif version in ['宙', '星']:
420 | ver = [plate_to_version['宙']]
421 | _ver = '宙&星'
422 | elif version in ['祭', '祝']:
423 | ver = [plate_to_version['祭']]
424 | _ver = '祭&祝'
425 | elif version in ['双', '宴']:
426 | ver = [plate_to_version['双']]
427 | _ver = '双&宴'
428 | else:
429 | ver = [plate_to_version[version]]
430 | _ver = version
431 |
432 | music_id_list = mai.total_plate_id_list[_ver]
433 | music = mai.total_list.by_id_list(music_id_list)
434 | plate_total_num = len(music_id_list)
435 | playerdata: List[PlayInfoDefault] = []
436 |
437 | obj = await maiApi.query_user_plate(qqid=qqid, version=ver)
438 | for _d in obj:
439 | if _d.song_id not in music_id_list:
440 | continue
441 | _music = mai.total_list.by_id(_d.song_id)
442 | _d.table_level = _music.level
443 | _d.ds = _music.ds[_d.level_index]
444 | playerdata.append(_d)
445 |
446 | ra: Dict[str, Dict[str, List[Optional[PlayInfoDefault]]]] = {}
447 | """
448 | {
449 | "14+": {
450 | "365": [None, None, None, PlayInfoDefault, None],
451 | ...
452 | },
453 | "14": {
454 | ...
455 | }
456 | }
457 | """
458 | music.sort(key=lambda x: x.ds[3], reverse=True)
459 | number = 4 if version not in ['霸', '舞'] else 5
460 | for _m in music:
461 | if _m.level[3] not in ra:
462 | ra[_m.level[3]] = {}
463 | ra[_m.level[3]][_m.id] = [None for _ in range(number)]
464 | for _d in playerdata:
465 | if number == 4 and _d.level_index == 4:
466 | continue
467 | ra[_d.table_level[3]][str(_d.song_id)][_d.level_index] = _d
468 |
469 | finished_bg = [Image.open(maimaidir / f't-{_}.png') for _ in range(4)]
470 | unfinished_bg = Image.open(maimaidir / 'unfinished_bg_2.png')
471 | complete_bg = Image.open(maimaidir / 'complete_bg_2.png')
472 |
473 | im = Image.open(platedir / f'{version}.png')
474 | draw = ImageDraw.Draw(im)
475 | tr = DrawText(draw, TBFONT)
476 | mr = DrawText(draw, SIYUAN)
477 |
478 | im.alpha_composite(Image.open(maimaidir / 'plate_num.png'), (185, 20))
479 | im.alpha_composite(
480 | Image.open(platedir / f'{version}{"極" if plan == "极" else plan}.png').resize((1000, 161)),
481 | (200, 35)
482 | )
483 | lv: List[set[int]] = [set() for _ in range(number)]
484 | y = 245
485 | # if plan == '者':
486 | # for level in ra:
487 | # x = 200
488 | # y += 15
489 | # for num, _id in enumerate(ra[level]):
490 | # if num % 10 == 0:
491 | # x = 200
492 | # y += 115
493 | # else:
494 | # x += 115
495 | # f: List[int] = []
496 | # for num, play in enumerate(ra[level][_id]):
497 | # if play.achievements or not play.achievements >= 80: continue
498 | # fc = Image.open(maimaidir / f'UI_MSS_MBase_Icon_{fcl[play.fc]}.png')
499 | # im.alpha_composite(fc, (x, y))
500 | # f.append(n)
501 | # for n in f:
502 | # im.alpha_composite(finished_bg[n], (x + 5 + 25 * n, y + 67))
503 | if plan == '极' or plan == '極':
504 | for level in ra:
505 | x = 200
506 | y += 15
507 | for num, _id in enumerate(ra[level]):
508 | if num % 10 == 0:
509 | x = 200
510 | y += 115
511 | else:
512 | x += 115
513 | f: List[int] = []
514 | for n, play in enumerate(ra[level][_id]):
515 | if play is None or not play.fc: continue
516 | if n == 3:
517 | im.alpha_composite(complete_bg, (x, y))
518 | fc = Image.open(maimaidir / f'UI_CHR_PlayBonus_{fcl[play.fc]}.png').resize((75, 75))
519 | im.alpha_composite(fc, (x + 13, y + 3))
520 | lv[n].add(play.song_id)
521 | f.append(n)
522 | for n in f:
523 | im.alpha_composite(finished_bg[n], (x + 5 + 25 * n, y + 67))
524 | if plan == '将':
525 | for level in ra:
526 | x = 200
527 | y += 15
528 | for num, _id in enumerate(ra[level]):
529 | if num % 10 == 0:
530 | x = 200
531 | y += 115
532 | else:
533 | x += 115
534 | f: List[int] = []
535 | for n, play in enumerate(ra[level][_id]):
536 | if play is None or play.achievements < 100: continue
537 | if n == 3:
538 | im.alpha_composite(complete_bg if play.achievements >= 100 else unfinished_bg, (x, y))
539 | rate = computeRa(play.ds, play.achievements, onlyrate=True)
540 | rank = Image.open(maimaidir / f'UI_TTR_Rank_{rate}.png').resize((102, 46))
541 | im.alpha_composite(rank, (x - 1, y + 15))
542 | lv[n].add(play.song_id)
543 | f.append(n)
544 | for n in f:
545 | im.alpha_composite(finished_bg[n], (x + 5 + 25 * n, y + 67))
546 | if plan == '神':
547 | _fc = ['ap', 'app']
548 | for level in ra:
549 | x = 200
550 | y += 15
551 | for num, _id in enumerate(ra[level]):
552 | if num % 10 == 0:
553 | x = 200
554 | y += 115
555 | else:
556 | x += 115
557 | f: List[int] = []
558 | for n, play in enumerate(ra[level][_id]):
559 | if play is None or play.fc not in _fc: continue
560 | if n == 3:
561 | im.alpha_composite(complete_bg, (x, y))
562 | ap = Image.open(maimaidir / f'UI_CHR_PlayBonus_{fcl[play.fc]}.png').resize((75, 75))
563 | im.alpha_composite(ap, (x + 13, y + 3))
564 | lv[n].add(play.song_id)
565 | f.append(n)
566 | for n in f:
567 | im.alpha_composite(finished_bg[n], (x + 5 + 25 * n, y + 67))
568 | if plan == '舞舞':
569 | fs = ['fsd', 'fdx', 'fsdp', 'fdxp']
570 | for level in ra:
571 | x = 200
572 | y += 15
573 | for num, _id in enumerate(ra[level]):
574 | if num % 10 == 0:
575 | x = 200
576 | y += 115
577 | else:
578 | x += 115
579 | f: List[int] = []
580 | for n, play in enumerate(ra[level][_id]):
581 | if play is None or play.fs not in fs:
582 | continue
583 | if n == 3:
584 | im.alpha_composite(complete_bg, (x, y))
585 | fsd = Image.open(maimaidir / f'UI_CHR_PlayBonus_{fsl[play.fs]}.png').resize((75, 75))
586 | im.alpha_composite(fsd, (x + 13, y + 3))
587 | lv[n].add(play.song_id)
588 | f.append(n)
589 | for n in f:
590 | im.alpha_composite(finished_bg[n], (x + 5 + 25 * n, y + 67))
591 |
592 | color = ScoreBaseImage.id_color.copy()
593 | color.insert(0, (124, 129, 255, 255))
594 | for num in range(len(lv) + 1):
595 | if num == 0:
596 | v = set.intersection(*lv)
597 | _v = f'{len(v)}/{plate_total_num}'
598 | else:
599 | _v = len(lv[num - 1])
600 | if _v == plate_total_num:
601 | mr.draw(390 + 200 * num, 270, 35, '完成', color[num], 'rm', 4, (255, 255, 255, 255))
602 | else:
603 | tr.draw(390 + 200 * num, 270, 40, _v, color[num], 'rm', 4, (255, 255, 255, 255))
604 |
605 | msg = MessageSegment.image(image_to_base64(im))
606 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
607 | msg = str(e)
608 | except Exception as e:
609 | log.error(traceback.format_exc())
610 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
611 | return msg
--------------------------------------------------------------------------------
/libraries/maimaidx_player_score.py:
--------------------------------------------------------------------------------
1 | import random
2 | import time
3 | import traceback
4 | from typing import Callable
5 |
6 | import pyecharts.options as opts
7 | from pyecharts.charts import Pie
8 |
9 | from hoshino.typing import MessageSegment
10 |
11 | from .. import *
12 | from .image import *
13 | from .maimai_best_50 import ScoreBaseImage, changeColumnWidth, coloumWidth, computeRa
14 | from .maimaidx_api_data import *
15 | from .maimaidx_model import PlanInfo, PlayInfoDefault, PlayInfoDev, RaMusic
16 | from .maimaidx_music import Music, mai
17 | from .tool import run_chrome_to_base64
18 |
19 | Filter = Tuple[
20 | List[PlayInfoDefault],
21 | List[PlayInfoDefault],
22 | List[PlayInfoDefault],
23 | List[PlayInfoDefault],
24 | List[PlayInfoDefault]
25 | ]
26 | Condition = Callable[[PlayInfoDefault], bool]
27 |
28 |
29 | async def music_global_data(music: Music, level_index: int) -> MessageSegment:
30 | """
31 | 绘制曲目游玩详情
32 |
33 | Params:
34 | `music`: :class:Music
35 | `level_index`: 难度
36 | Returns:
37 | `MessageSegment`
38 | """
39 | stats = music.stats[level_index]
40 | fc_data_pair = [list(z) for z in zip([c.upper() if c else 'Not FC' for c in [''] + comboRank], stats.fc_dist)]
41 | acc_data_pair = [list(z) for z in zip([s.upper() for s in scoreRank], stats.dist)]
42 |
43 | initopts = opts.InitOpts(width='1000px', height='800px', bg_color='#fff', js_host='./')
44 | labelopts = opts.LabelOpts(
45 | position='outside',
46 | formatter='{a|{a}}{abg|}\n{hr|}\n {b|{b}: }{c} {per|{d}%} ',
47 | background_color='#eee',
48 | border_color='#aaa',
49 | border_width=1,
50 | border_radius=4,
51 | rich={
52 | 'a': {'color': '#999', 'lineHeight': 22, 'align': 'center'},
53 | 'abg': {
54 | 'backgroundColor': '#e3e3e3',
55 | 'width': '100%',
56 | 'align': 'right',
57 | 'height': 22,
58 | 'borderRadius': [4, 4, 0, 0],
59 | },
60 | 'hr': {
61 | 'borderColor': '#aaa',
62 | 'width': '100%',
63 | 'borderWidth': 0.5,
64 | 'height': 0,
65 | },
66 | 'b': {'fontSize': 16, 'lineHeight': 33},
67 | 'per': {
68 | 'color': '#eee',
69 | 'backgroundColor': '#334455',
70 | 'padding': [2, 4],
71 | 'borderRadius': 2,
72 | },
73 | },
74 | )
75 | titleopts = opts.TitleOpts(
76 | title=f'{music.id} {music.title} 「{diffs[level_index]}」',
77 | pos_left='center',
78 | pos_top='20',
79 | title_textstyle_opts=opts.TextStyleOpts(color='#2c343c'),
80 | )
81 | legendopts = opts.LegendOpts(pos_left=15, pos_top=10, orient='vertical')
82 |
83 | pie = Pie(initopts)
84 | pie.add('全连等级', fc_data_pair, radius=[0, '30%'], label_opts=labelopts)
85 | pie.add('达成率等级', acc_data_pair, radius=['50%', '70%'], is_clockwise=True, label_opts=labelopts)
86 | pie.set_global_opts(title_opts=titleopts, legend_opts=legendopts)
87 | pie.set_series_opts(tooltip_opts=opts.TooltipOpts(trigger='item', formatter='{a}
{b}: {c} ({d}%)'))
88 | pie.render(str(pie_html_file))
89 | base64 = await run_chrome_to_base64()
90 |
91 | return MessageSegment.image(base64)
92 |
93 |
94 | class DrawScore(ScoreBaseImage):
95 |
96 | def __init__(self, image: Image.Image = None) -> None:
97 | super().__init__(image)
98 | self._im.alpha_composite(self.aurora_bg)
99 | self._im.alpha_composite(self.shines_bg, (34, 0))
100 | self._im.alpha_composite(self.rainbow_bg, (319, self._im.size[1] - 643))
101 | self._im.alpha_composite(self.rainbow_bottom_bg, (100, self._im.size[1] - 343))
102 | for h in range((self._im.size[1] // 358) + 1):
103 | self._im.alpha_composite(self.pattern_bg, (0, (358 + 7) * h))
104 |
105 | def whilepic(self, data: List[RaMusic], y: int = 200):
106 | """
107 | 循环绘制谱面
108 |
109 | Params:
110 | `data`: `谱面数据`
111 | `y`: `Y轴偏移`
112 | """
113 | dy = 65
114 | x = 0
115 | for n, v in enumerate(data):
116 | if n % 20 == 0:
117 | x = 55
118 | y += dy if n != 0 else 0
119 | else:
120 | x += 65
121 | cover = Image.open(music_picture(v.id)).resize((55, 55))
122 | self._im.alpha_composite(cover, (x, y))
123 | self._im.alpha_composite(self.id_diff[int(v.lv)], (x, y + 45))
124 | self._tb.draw(x + 27, y + 50, 10, v.id, self.t_color[int(v.lv)], 'mm')
125 |
126 | def whilerisepic(self, data: List[RiseScore], low_score: int, isdx: bool):
127 | """
128 | 循环绘制上分推荐数据
129 |
130 | Params:
131 | `data`: `上分数据`
132 | `low_score`: `最低分`
133 | `isdx`: `是否DX版本`
134 | """
135 | y = 120
136 | for index, _d in enumerate(data):
137 | x = 200 if isdx else 700
138 | y += 140 if index != 0 else 0
139 |
140 | rate = Image.open(maimaidir / f'UI_TTR_Rank_{_d.rate}.png').resize((63, 28))
141 |
142 | self._im.alpha_composite(self._rise[_d.level_index], (x + 30, y))
143 | self._im.alpha_composite(Image.open(music_picture(_d.song_id)).resize((80, 80)), (x + 55, y + 40))
144 | self._im.alpha_composite(Image.open(maimaidir / f'{_d.type.upper()}.png').resize((60, 22)), (x + 240, y + 114))
145 | if _d.oldrate:
146 | oldrate = Image.open(maimaidir / f'UI_TTR_Rank_{_d.oldrate}.png').resize((63, 28))
147 | self._im.alpha_composite(oldrate, (x + 145, y + 82))
148 | self._im.alpha_composite(rate, (x + 305, y + 82))
149 |
150 | title = _d.title
151 | if coloumWidth(title) > 26:
152 | title = changeColumnWidth(title, 25) + '...'
153 | self._sy.draw(x + 142, y + 44, 17, title, self.t_color[_d.level_index], 'lm')
154 | self._tb.draw(x + 145, y + 124, 18, f'ID: {_d.song_id}', self.id_color[_d.level_index], 'lm')
155 | self._tb.draw(x + 210, y + 71, 25, f'{_d.oldachievements:.4f}%', self.t_color[_d.level_index], anchor='mm')
156 | self._tb.draw(x + 245, y + 96, 17, f'Ra: {_d.oldra}', self.t_color[_d.level_index], anchor='mm')
157 | self._tb.draw(x + 370, y + 71, 25, f'{_d.achievements:.4f}%', self.t_color[_d.level_index], anchor='mm')
158 | self._tb.draw(x + 415, y + 96, 17, f'Ra: {_d.ra}', self.t_color[_d.level_index], anchor='mm')
159 | self._tb.draw(x + 315, y + 124, 18, f'ds:{_d.ds}', self.id_color[_d.level_index], anchor='lm')
160 | if _d.oldra > low_score:
161 | new_ra = _d.ra - _d.oldra
162 | else:
163 | new_ra = _d.ra - low_score
164 | self._tb.draw(x + 390, y + 124, 18, f'Ra +{new_ra}', self.id_color[_d.level_index], 'lm')
165 |
166 | def draw_rise(self, sd: List[RiseScore], sd_score: int, dx: List[RiseScore], dx_score: int) -> Image.Image:
167 | """
168 | 绘制上分数据表
169 |
170 | Params:
171 | `sd`: `旧版本谱面`
172 | `sd_score`: `旧版本最低分`
173 | `sd`: `新版本谱面`
174 | `dx_score`: `新版本最低分`
175 | Returns:
176 | `Image.Image`
177 | """
178 | title_bg = self.title_bg.copy().resize((273, 80))
179 | self._im.alpha_composite(title_bg, (314, 30))
180 | self._sy.draw(450, 68, 18, '旧版本谱面推荐', self.text_color, 'mm')
181 | self.whilerisepic(sd, sd_score, True)
182 | self._im.alpha_composite(title_bg, (814, 30))
183 | self._sy.draw(950, 68, 18, '新版本谱面推荐', self.text_color, 'mm')
184 | self.whilerisepic(dx, dx_score, False)
185 |
186 | height = self._im.size[1]
187 | self._im.alpha_composite(self.design_bg.resize((800, 72)), (300, height - 110))
188 | self._sy.draw(700, height - 76, 18, f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} BOT', self.text_color, 'mm')
189 | return self._im
190 |
191 | def draw_plan(
192 | self,
193 | completed: Union[List[PlayInfoDefault], List[PlayInfoDev]],
194 | completed_y: int,
195 | unfinished: Union[List[PlayInfoDefault], List[PlayInfoDev]],
196 | unfinished_y: int,
197 | notstarted: List[RaMusic],
198 | plan: str,
199 | completed_len: int
200 | ) -> Image.Image:
201 | """
202 | 绘制进度表
203 |
204 | Params:
205 | `completed`: `已完成谱面`
206 | `completed_y`: `已完成谱面高度`
207 | `unfinished`: `未完成谱面`
208 | `unfinished_y`: `未完成谱面高度`
209 | `notstarted`: `未游玩谱面`
210 | `plan`: `目标`
211 | `completed_len`: `已完成谱面数量`
212 | Returns:
213 | `Image.Image`
214 | """
215 | max = len(completed + unfinished + notstarted)
216 |
217 | self._im.alpha_composite(self.title_lengthen_bg, (475, 30))
218 | self._im.alpha_composite(self.title_lengthen_bg, (475, 30 + completed_y))
219 | self._im.alpha_composite(self.title_lengthen_bg, (475, 30 + completed_y + unfinished_y))
220 |
221 | self._sy.draw(700, 77, 22, f'已完成谱面「{len(completed)}」个', self.text_color, 'mm')
222 | self._sy.draw(700, 77 + completed_y, 22, f'未完成谱面「{len(unfinished)}」个', self.text_color, 'mm')
223 | self._sy.draw(700, 77 + completed_y + unfinished_y, 22, f'未游玩谱面「{len(notstarted)}」个', self.text_color, 'mm')
224 |
225 | self.whiledraw(completed[:completed_len], True, 140)
226 | self.whiledraw(unfinished[:30], True, 140 + completed_y)
227 | self.whilepic(notstarted[:100], 140 + completed_y + unfinished_y)
228 |
229 | self._im.alpha_composite(self.design_bg, (200, self._im.size[1] - 113))
230 | pagemsg = f'共计「{max}」个谱面,剩余「{len(unfinished + notstarted)}」个谱面未完成「{plan.upper()}」'
231 | self._sy.draw(700, self._im.size[1] - 70, 25, pagemsg, self.text_color, 'mm')
232 | return self._im
233 |
234 | def draw_category(
235 | self,
236 | category: str,
237 | data: Union[List[PlayInfoDefault], List[PlayInfoDev], List[RaMusic]],
238 | page: int = 1,
239 | end_page: int = 1
240 | ) -> Image.Image:
241 | """
242 | 绘制指定进度表
243 |
244 | Params:
245 | `category`: `类别`
246 | `data`: `数据`
247 | `page`: `页数`
248 | `end_page`: `总页数`
249 | Returns:
250 | `Image.Image`
251 | """
252 | lendata = len(data)
253 | newdata = data[(page - 1) * 80: page * 80]
254 | self._im.alpha_composite(self.title_lengthen_bg, (475, 30))
255 | if category == 'completed' or category == 'unfinished':
256 | txt = '已完成' if category == 'completed' else '未完成'
257 | self._sy.draw(700, 77, 28, f'{txt}谱面', self.text_color, 'mm')
258 | self.whiledraw(newdata, True, 140)
259 | self._im.alpha_composite(self.design_bg, (200, self._im.size[1] - 113))
260 |
261 | pagemsg = f'{txt}谱面共计「{lendata}」个,'
262 | pagemsg += f'展示第「{(page - 1) * 80 + 1}-{80 * (page - 1) + len(newdata)}」个,'
263 | pagemsg += f'当前第「{page} / {end_page}」页'
264 | self._sy.draw(700, self._im.size[1] - 70, 25, pagemsg, self.text_color, 'mm')
265 | else:
266 | self._sy.draw(700, 77, 28, '未游玩谱面', self.text_color, 'mm')
267 | self.whilepic(data)
268 | self._im.alpha_composite(self.design_bg, (200, self._im.size[1] - 113))
269 | self._sy.draw(700, self._im.size[1] - 70, 25, f'未游玩谱面共计「{len(data)}」个', self.text_color, 'mm')
270 | return self._im
271 |
272 | def draw_scorelist(
273 | self,
274 | rating: Union[str, float],
275 | data: Union[List[PlayInfoDefault], List[PlayInfoDev]],
276 | page: int = 1,
277 | end_page: int = 1
278 | ) -> Image.Image:
279 | """
280 | 绘制分数列表
281 |
282 | Params:
283 | `rating`: `定数`
284 | `data`: `数据`
285 | `page`: `页数`
286 | `end_page`: `总页数`
287 | Returns:
288 | `Image.Image`
289 | """
290 | lendata = len(data)
291 | newdata = data[(page - 1) * 80: page * 80]
292 | r = len(newdata) // 20 + (0 if len(newdata) % 20 == 0 else 1)
293 | for n in range(r):
294 | y = (109 * 4 + 140) * n
295 | self._im.alpha_composite(self.title_lengthen_bg, (475, 30 + y))
296 | start = (20 * n + 1) + 80 * (page - 1)
297 | self._sy.draw(700, 77 + y, 28, f'No.{start}- No.{start + len(newdata[n * 20: (n + 1) * 20]) - 1}', self.text_color, 'mm')
298 | self.whiledraw(newdata[n * 20: (n + 1) * 20], True, 140 + y)
299 | self._im.alpha_composite(self.design_bg, (200, self._im.size[1] - 113))
300 |
301 | pagemsg = f'「{rating}」共计「{lendata}」个成绩,'
302 | pagemsg += f'展示第「{(page - 1) * 80 + 1}-{80 * (page - 1) + len(newdata)}」个,'
303 | pagemsg += f'当前第「{page} / {end_page}」页'
304 | self._sy.draw(700, self._im.size[1] - 70, 25, pagemsg, self.text_color, 'mm')
305 | return self._im
306 |
307 |
308 | def get_rise_score_list(
309 | old_records: Dict[int, Dict[str, Union[int, float]]],
310 | type: str,
311 | info: List[ChartInfo],
312 | level: Optional[str] = None,
313 | score: Optional[int] = None
314 | ) -> Tuple[List[RiseScore], int]:
315 | """
316 | 随机获取加分曲目
317 |
318 | Params:
319 | `type`: 版本
320 | `info`: 游玩成绩列表
321 | `level`: 等级
322 | `score`: 分数
323 | Returns:
324 | `Tuple[List[RiseScore], int]`
325 | """
326 | ignore = [m.song_id for m in info if m.achievements >= 100.5]
327 | ra = info[-1].ra
328 | music: List[RiseScore] = []
329 | if score is None:
330 | ss_ds = round(ra / 20.8, 1)
331 | else:
332 | ss_ds = round((ra + score) / 20.8, 1)
333 | sssp_ds = round(ra / 22.4, 1)
334 | ds = (sssp_ds + 0.1, ss_ds + 0.1)
335 | version = list(plate_to_version.values())[-2] if type == 'DX' else list(plate_to_version.values())[:-2]
336 | musiclist = mai.total_list.filter(level=level, ds=ds, version=version)
337 | for _m in musiclist:
338 | if (song_id := int(_m.id)) in ignore:
339 | continue
340 | if song_id >= 100000:
341 | continue
342 | for index in _m.diff:
343 | for r in achievementList[-4:]:
344 | basera, rate = computeRa(_m.ds[index], r, israte=True)
345 | if basera <= ra:
346 | continue
347 | if score and basera - score < ra:
348 | continue
349 | if song_id in old_records and old_records[song_id]['level_index'] == index:
350 | oldra, oldrate = computeRa(_m.ds[index], old_records[song_id]['achievements'], israte=True)
351 | if oldra >= basera:
352 | continue
353 | ss = RiseScore(
354 | song_id=song_id,
355 | title=_m.title,
356 | type=_m.type,
357 | level_index=index,
358 | ds=_m.ds[index],
359 | ra=basera,
360 | rate=rate,
361 | achievements=r,
362 | oldra=oldra,
363 | oldrate=oldrate,
364 | oldachievements=old_records[song_id]['achievements']
365 | )
366 | else:
367 | ss = RiseScore(
368 | song_id=song_id,
369 | title=_m.title,
370 | type=_m.type,
371 | level_index=index,
372 | ds=_m.ds[index],
373 | ra=basera,
374 | rate=rate,
375 | achievements=r
376 | )
377 | music.append(ss)
378 | break
379 | if not music:
380 | return music, 0
381 | new = random.sample(music, musiclen if 0 < (musiclen := len(musiclist)) < 5 else 5)
382 | new.sort(key=lambda x: x.song_id, reverse=True)
383 | return new, ra
384 |
385 |
386 | async def rise_score_data(
387 | qqid: int,
388 | username: Optional[str] = None,
389 | level: Optional[str] = None,
390 | score: Optional[int] = None
391 | ) -> Union[MessageSegment, str]:
392 | """
393 | 上分数据
394 |
395 | Params:
396 | `qqid`: 用户QQ
397 | `username`: 查分器用户名
398 | `level`: 定数
399 | `score`: 分数
400 | Returns:
401 | `Union[Image.Image, str]`
402 | """
403 | try:
404 | user = await maiApi.query_user_b50(qqid=qqid, username=username)
405 | records = await maiApi.query_user_plate(qqid=qqid, username=username, version=list(plate_to_version.values()))
406 | old_records: Dict[int, Dict[str, Union[int, float]]] = {
407 | m.song_id: {
408 | 'level_index': m.level_index,
409 | 'achievements': m.achievements
410 | } for m in records
411 | }
412 |
413 | sd, sd_low_score = get_rise_score_list(old_records, 'SD', user.charts.sd, level, score)
414 | dx, dx_low_score = get_rise_score_list(old_records, 'DX', user.charts.dx, level, score)
415 |
416 | if not sd and not dx:
417 | return '没有推荐的铺面'
418 |
419 | lensd, lendx = len(sd), len(dx)
420 |
421 | h = max(lensd, lendx)
422 | height = h * 140 + 110 + 150
423 | image = tricolor_gradient(1400, height)
424 |
425 | ds = DrawScore(image)
426 | im = ds.draw_rise(sd, sd_low_score, dx, dx_low_score).crop((200, 0, 1200, height))
427 |
428 | msg = MessageSegment.image(image_to_base64(im))
429 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
430 | msg = str(e)
431 | except Exception as e:
432 | log.error(traceback.format_exc())
433 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
434 |
435 | return msg
436 |
437 |
438 | def plate_message(
439 | result: str,
440 | plan: str,
441 | music_list: List[PlayInfoDefault],
442 | played: List[Tuple[int, int]]
443 | ) -> Union[MessageSegment, str]:
444 | """
445 | Params:
446 | `result`: 结果
447 | `plan`: 目标
448 | `music_list`: 谱面列表
449 | `played`: 已游玩谱面
450 | Returns:
451 | `Union[MessageSegment, str]`
452 | """
453 | for n, m in enumerate(music_list):
454 | self_record = ''
455 | if (m.song_id, m.level_index) in played:
456 | if plan in ['将', '者']:
457 | self_record = f'{m.achievements}%'
458 | if plan in ['極', '极', '神']:
459 | self_record = m.fc
460 | if plan in '舞舞':
461 | self_record = m.fs
462 | result += f'No.{n + 1:02d} {f"「{m.song_id}」":>7} {f"「{diffs[m.level_index]}」":>11} 「{m.ds}」 {m.title} {self_record}\n'
463 | if len(music_list) > 10:
464 | result = MessageSegment.image(image_to_base64(text_to_image(result.strip())))
465 | return result
466 |
467 |
468 | async def player_plate_data(qqid: int, username: str, version: str, plan: str) -> Union[MessageSegment, str]:
469 | """
470 | 查看牌子进度
471 |
472 | Params:
473 | `qqid`: 用户QQ
474 | `username`: 查分器用户名
475 | `version`: 版本
476 | `plan`: 目标
477 | Returns:
478 | `Union[MessageSegment, str]`
479 | """
480 | if version in platecn:
481 | version = platecn[version]
482 | if version == '真':
483 | ver = [plate_to_version['真']] + [plate_to_version['初']]
484 | _ver = version
485 | elif version in ['霸', '舞']:
486 | ver = list(set(_v for _v in list(plate_to_version.values())[:-9]))
487 | _ver = '舞'
488 | elif version in ['熊', '华', '華']:
489 | ver = [plate_to_version['熊']]
490 | _ver = '熊&华'
491 | elif version in ['爽', '煌']:
492 | ver = [plate_to_version['爽']]
493 | _ver = '爽&煌'
494 | elif version in ['宙', '星']:
495 | ver = [plate_to_version['宙']]
496 | _ver = '宙&星'
497 | elif version in ['祭', '祝']:
498 | ver = [plate_to_version['祭']]
499 | _ver = '祭&祝'
500 | elif version in ['双', '宴']:
501 | ver = [plate_to_version['双']]
502 | _ver = '双&宴'
503 | else:
504 | ver = [plate_to_version[version]]
505 | _ver = version
506 |
507 | try:
508 | verlist = await maiApi.query_user_plate(qqid=qqid, username=username, version=ver)
509 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
510 | return str(e)
511 |
512 | if plan in ['将', '者']:
513 | achievement = 100 if plan == '将' else 80
514 | callable_: Condition = lambda x: x.achievements < achievement
515 | elif plan in ['極', '极']:
516 | callable_: Condition = lambda x: not x.fc
517 | elif plan == '舞舞':
518 | callable_: Condition = lambda x: x.fs not in ['fsd', 'fsdp']
519 | elif plan == '神':
520 | callable_: Condition = lambda x: x.fc not in ['ap', 'app']
521 | else:
522 | raise ValueError
523 |
524 | unfinished_model_list: Filter = ([], [], [], [], [])
525 | unfinished: List[Tuple[int, int]] = []
526 | played: List[Tuple[int, int]] = []
527 | remaster: List[int] = []
528 |
529 | # 已游玩未完成曲目
530 | plate_id_list = mai.total_plate_id_list[_ver]
531 | if version in ['舞', '霸']:
532 | remaster = mai.total_plate_id_list['舞ReMASTER']
533 | for music in verlist:
534 | if music.song_id not in plate_id_list:
535 | continue
536 | if music.level_index == 4 and music.song_id not in remaster:
537 | continue
538 | if callable_(music):
539 | unfinished.append((music.song_id, music.level_index))
540 | played.append((music.song_id, music.level_index))
541 | else:
542 | for music in verlist:
543 | if music.song_id not in plate_id_list:
544 | continue
545 | if callable_(music):
546 | unfinished.append((music.song_id, music.level_index))
547 | played.append((music.song_id, music.level_index))
548 |
549 | # 未游玩未完成曲目
550 | for music in mai.total_list:
551 | if int(music.id) not in plate_id_list:
552 | continue
553 | info = PlayInfoDefault(
554 | achievements=0,
555 | level='',
556 | level_index=0,
557 | title=music.title,
558 | type=music.type,
559 | id=int(music.id)
560 | )
561 | range_ = range(5 if version in ['舞', '霸'] and int(music.id) in remaster else 4)
562 | for level_index in range_:
563 | if (m := (info.song_id, level_index)) not in played or m in unfinished:
564 | _info = info.model_copy()
565 | _info.level = music.level[level_index]
566 | _info.ds = music.ds[level_index]
567 | _info.level_index = level_index
568 | unfinished_model_list[level_index].append(_info)
569 |
570 | basic, advanced, expert, master, re_master = unfinished_model_list
571 |
572 | ramain = basic + advanced + expert + master + re_master
573 | ramain.sort(key=lambda x: x.ds, reverse=True)
574 | difficult = [_m for _m in ramain if _m.ds > 13.6]
575 |
576 | appellation = username if username else '您'
577 | result = dedent(f'''\
578 | {appellation}的「{version}{plan}」剩余进度如下:
579 | Basic剩余「{len(basic)}」首
580 | Advanced剩余「{len(advanced)}」首
581 | Expert剩余「{len(expert)}」首
582 | Master剩余「{len(master)}」首
583 | ''')
584 | if version in ['舞', '霸']:
585 | result += f'Re:Master剩余「{len(re_master)}」首\n'
586 |
587 | if len(difficult) > 0:
588 | if len(difficult) < 60:
589 | result += '剩余定数大于13.6的曲目:\n'
590 | result = plate_message(result, plan, difficult, played)
591 | else:
592 | result += f'还有{len(difficult)}首大于13.6定数的曲目,加油推分捏!\n'
593 | elif len(ramain) > 0:
594 | if len(ramain) < 60:
595 | result += '剩余曲目:\n'
596 | result = plate_message(result, plan, ramain, played)
597 | else:
598 | result += '已经没有定数大于13.6的曲目了,加油清谱捏!\n'
599 | else:
600 | result = f'已经没有剩余的的曲目了,恭喜{appellation}完成「{version}{plan}」!'
601 | return result
602 |
603 |
604 | def calc(data: dict) -> Union[PlayInfoDefault, PlayInfoDev]:
605 | if not maiApi.token:
606 | _m = mai.total_list.by_id(data['id'])
607 | ds: float = _m.ds[data['level_index']]
608 | a: float = data['achievements']
609 | ra, rate = computeRa(ds, a, israte=True)
610 | info = PlayInfoDefault(**data, ds=ds, ra=ra, rate=rate)
611 | else:
612 | info = PlayInfoDev(**data)
613 | return info
614 |
615 |
616 | async def level_process_data(
617 | qqid: int,
618 | username: Optional[str],
619 | level: str,
620 | plan: str,
621 | category: str = 'default',
622 | page: int = 1
623 | ) -> Union[MessageSegment, str]:
624 | """
625 | 查看谱面等级进度
626 |
627 | Params:
628 | `qqid`: 用户QQ
629 | `username`: 查分器用户名
630 | `level`: 定数
631 | `plan`: 评价等级
632 | Returns:
633 | `Union[MessageSegment, str]`
634 | """
635 | try:
636 | if maiApi.token:
637 | devobj = await maiApi.query_user_get_dev(qqid=qqid, username=username)
638 | obj = devobj.records
639 | else:
640 | version = list(set(_v for _v in list(plate_to_version.values())))
641 | obj = await maiApi.query_user_plate(qqid=qqid, username=username, version=version)
642 | music = mai.total_list.by_plan(level)
643 |
644 | planlist = [0, 0, 0]
645 | plannum = 0
646 | if plan.lower() in scoreRank:
647 | plannum = 0
648 | planlist[0] = achievementList[scoreRank.index(plan.lower()) - 1]
649 | elif plan.lower() in comboRank:
650 | plannum = 1
651 | planlist[1] = comboRank.index(plan.lower())
652 | elif plan.lower() in syncRank:
653 | plannum = 2
654 | planlist[2] = syncRank.index(plan.lower())
655 | else:
656 | raise
657 |
658 | for _d in obj:
659 | if isinstance(_d, PlayInfoDefault):
660 | _m = mai.total_list.by_id(_d.song_id)
661 | ds: float = _m.ds[_d.level_index]
662 | a: float = _d.achievements
663 | ra, rate = computeRa(ds, a, israte=True)
664 | _d.ra = ra
665 | _d.rate = rate
666 | if (song_id := str(_d.song_id)) in music and _d.level == level:
667 | if isinstance(music[song_id], Dict):
668 | music[song_id][_d.level_index] = PlanInfo()
669 | _p = music[song_id][_d.level_index]
670 | else:
671 | music[song_id] = PlanInfo()
672 | _p = music[song_id]
673 |
674 | if (plannum == 0 and _d.achievements >= planlist[plannum]) or \
675 | (plannum == 1 and _d.fc and combo_rank.index(_d.fc) >= planlist[plannum]) or \
676 | (plannum == 2 and _d.fs and (sync_rank.index(_d.fs) >= planlist[plannum] if _d.fs and _d.fs in sync_rank else sync_rank_p.index(_d.fs) >= planlist[plannum])):
677 | _p.completed = _d
678 | else:
679 | _p.unfinished = _d
680 |
681 | notplayed: List[RaMusic] = []
682 | completed: Union[List[PlayInfoDefault], List[PlayInfoDev]] = []
683 | unfinished: Union[List[PlayInfoDefault], List[PlayInfoDev]] = []
684 | for m in music:
685 | play = music[m]
686 | if isinstance(play, Dict):
687 | for index, p in play.items():
688 | if isinstance(p, RaMusic):
689 | notplayed.append(p)
690 | elif p.completed:
691 | completed.append(p.completed)
692 | elif p.unfinished:
693 | unfinished.append(p.unfinished)
694 | elif isinstance(play, PlanInfo):
695 | if play.completed:
696 | completed.append(play.completed)
697 | if play.unfinished:
698 | unfinished.append(play.unfinished)
699 | else:
700 | notplayed.append(play)
701 | completed.sort(key=lambda x: x.achievements if plannum == 0 else x.fc if plannum == 1 else x.fs, reverse=True)
702 | unfinished.sort(key=lambda x: x.achievements if plannum == 0 else x.fc if plannum == 1 else x.fs, reverse=True)
703 | notplayed.sort(key=lambda x: x.ds, reverse=True)
704 |
705 | if category == 'default':
706 | completed_len = 60 if len(unfinished) == 0 and len(notplayed) == 0 else 30
707 | clen = len(completed[:completed_len])
708 | completed_y = (clen // 5 + (0 if clen % 5 == 0 else 1)) * 109 + 140
709 | ulen = len(unfinished[:30])
710 | unfinished_y = (ulen // 5 + (0 if ulen % 5 == 0 else 1)) * 109 + 140
711 | nlen = len(notplayed[:100])
712 | notstarted_y = (nlen // 20 + (0 if nlen % 20 == 0 else 1)) * 65 + 140
713 | image = tricolor_gradient(1400, 150 + completed_y + unfinished_y + notstarted_y)
714 | dp = DrawScore(image)
715 | im = dp.draw_plan(completed, completed_y, unfinished, unfinished_y, notplayed, plan, completed_len)
716 | elif category == 'completed' or category == 'unfinished':
717 | data = completed if category == 'completed' else unfinished
718 | lendata = len(data)
719 | end_page_num = lendata // 80 + 1
720 | if page > end_page_num:
721 | return f'超出页数,您的成绩共计「{end_page_num}」页,请重新输入'
722 | topage = len(data[(page - 1) * 80: page * 80])
723 | plc = (topage // 5 + (0 if topage % 5 == 0 else 1)) * 109
724 | image = tricolor_gradient(1400, 240 + plc + 120)
725 | dp = DrawScore(image)
726 | im = dp.draw_category(category, data, page, end_page_num)
727 | else:
728 | lennotstarted = len(notplayed)
729 | pln = (lennotstarted // 20 + (0 if lennotstarted % 20 == 0 else 1)) * 65
730 | image = tricolor_gradient(1400, 240 + pln + 120)
731 | dp = DrawScore(image)
732 | im = dp.draw_category(category, notplayed)
733 |
734 | msg = MessageSegment.image(image_to_base64(im))
735 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
736 | msg = str(e)
737 | except Exception as e:
738 | log.error(traceback.format_exc())
739 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
740 | return msg
741 |
742 |
743 | async def level_achievement_list_data(
744 | qqid: int,
745 | username: Optional[str],
746 | rating: Union[str, float],
747 | page: int = 1
748 | ) -> Union[MessageSegment, str]:
749 | """
750 | 查看分数列表
751 |
752 | Params:
753 | `qqid` : 用户QQ
754 | `username` : 查分器用户名
755 | `rating` : 定数
756 | `page` : 页数
757 | `nickname` : 用户昵称
758 | Returns:
759 | `Union[MessageSegment, str]
760 | """
761 | try:
762 | data: Union[List[PlayInfoDefault], List[PlayInfoDev]] = []
763 | if maiApi.token:
764 | obj = await maiApi.query_user_get_dev(qqid=qqid, username=username)
765 | data = obj.records
766 | else:
767 | version = list(set(_v for _v in list(plate_to_version.values())))
768 | obj = await maiApi.query_user_plate(qqid=qqid, username=username, version=version)
769 | for _d in obj:
770 | music = mai.total_list.by_id(_d.song_id)
771 | _d.ds = music.ds[_d.level_index]
772 | _d.ra, _d.rate = computeRa(_d.ds, _d.achievements, israte=True)
773 | data = obj
774 |
775 | if isinstance(rating, str):
776 | newdata = sorted(list(filter(lambda x: x.level == rating, data)), key=lambda z: z.achievements, reverse=True)
777 | else:
778 | newdata = sorted(list(filter(lambda x: x.ds == rating, data)), key=lambda z: z.achievements, reverse=True)
779 |
780 | lendata = len(newdata)
781 | end_page_num = lendata // 80 + 1
782 | if page > end_page_num:
783 | return f'超出页数,您的成绩共计「{end_page_num}」页,请重新输入'
784 |
785 | topage = len(newdata[(page - 1) * 80: page * 80])
786 | line = topage // 5 + (0 if topage % 5 == 0 else 1)
787 | if page < end_page_num:
788 | plc = line * 109 + 140 * 4
789 | elif topage <= 20:
790 | plc = 4 * 109 + 140
791 | elif topage <= 40:
792 | plc = line * 109 + 140 * 2
793 | elif topage <= 60:
794 | plc = line * 109 + 140 * 3
795 | else:
796 | plc = line * 109 + 140 * 4
797 |
798 | image = tricolor_gradient(1400, 150 + plc)
799 |
800 | sc = DrawScore(image)
801 | im = sc.draw_scorelist(rating, newdata, page, end_page_num)
802 | msg = MessageSegment.image(image_to_base64(im))
803 | except (UserNotFoundError, UserNotExistsError, UserDisabledQueryError) as e:
804 | msg = str(e)
805 | except Exception as e:
806 | log.error(traceback.format_exc())
807 | msg = f'未知错误:{type(e)}\n请联系Bot管理员'
808 | return msg
809 |
810 |
811 | async def rating_ranking_data(name: str, page: int) -> Union[MessageSegment, str]:
812 | """
813 | 查看查分器排行榜
814 |
815 | Params:
816 | `name`: 指定用户名
817 | `page`: 页数
818 | Returns:
819 | `Union[MessageSegment, str]`
820 | """
821 | try:
822 | rank_data = await maiApi.rating_ranking()
823 |
824 | _time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
825 | if name != '':
826 | if name in [r.username.lower() for r in rank_data]:
827 | rank_index = [r.username.lower() for r in rank_data].index(name) + 1
828 | nickname = rank_data[rank_index - 1].username
829 | data = f'截止至 {_time}\n玩家 {nickname} 在查分器已注册用户ra排行第{rank_index}'
830 | else:
831 | data = '未找到该玩家'
832 | else:
833 | user_num = len(rank_data)
834 | msg = f'截止至 {_time},查分器已注册用户ra排行:\n'
835 | if page * 50 > user_num:
836 | page = user_num // 50 + 1
837 | end = page * 50 if page * 50 < user_num else user_num
838 | for i, ranker in enumerate(rank_data[(page - 1) * 50:end]):
839 | msg += f'No.{i + 1 + (page - 1) * 50:02d}.「{ranker.ra}」 {ranker.username} \n'
840 | msg += f'第「{page}」页,共「{user_num // 50 + 1}」页'
841 | data = MessageSegment.image(image_to_base64(text_to_image(msg.strip())))
842 | except Exception as e:
843 | log.error(traceback.format_exc())
844 | data = f'未知错误:{type(e)}\n请联系Bot管理员'
845 | return data
--------------------------------------------------------------------------------
/libraries/maimaidx_update_table.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import time
3 |
4 | import aiofiles
5 |
6 | from .image import tricolor_gradient
7 | from .maimai_best_50 import *
8 | from .maimaidx_music import Music, mai
9 |
10 |
11 | async def update_rating_table() -> str:
12 | """更新定数表"""
13 | try:
14 | dx = Image.open(maimaidir / 'DX.png').convert('RGBA').resize((44, 16))
15 | diff = [Image.new('RGBA', (75, 16), color) for color in ScoreBaseImage.bg_color]
16 | atime = 0
17 | for lv in levelList[5:]:
18 | _otime = time.time()
19 | picname = ratingdir / f'{lv}.png'
20 | lvlist = mai.total_level_data[lv]
21 | lines = 0
22 | for _lv in lvlist:
23 | musicnum = len(lvlist[_lv])
24 | if musicnum == 0:
25 | r = 1
26 | else:
27 | remainder = musicnum % 14
28 | r = (musicnum // 14) + (1 if remainder else 0)
29 | lines += r
30 |
31 | if '+' in lv:
32 | f = 3
33 | elif lv == '6':
34 | f = 10
35 | else:
36 | f = 7
37 |
38 | linesheight = 85 * lines
39 | """
40 | `85` 为曲绘高度 `80` + 间隔 `5`
41 | `lines` 为行数
42 | """
43 |
44 | width, height = 1400, 325 + f * 20 + linesheight
45 | """
46 | `325` 为顶部文字和底部图片高度 + 上下间隔高度
47 | `f * 20` 为等级数量 `f` * 等级间隔 `20`
48 | `linesheight` 为各等级曲绘和间隔总和高度
49 | """
50 |
51 | im = tricolor_gradient(width, height)
52 |
53 | im.alpha_composite(ScoreBaseImage.aurora_bg)
54 | im.alpha_composite(ScoreBaseImage.shines_bg, (34, 0))
55 | im.alpha_composite(ScoreBaseImage.rainbow_bg, (319, height - 643))
56 | im.alpha_composite(ScoreBaseImage.rainbow_bottom_bg, (100, height - 343))
57 | for h in range((height // 358) + 1):
58 | im.alpha_composite(ScoreBaseImage.pattern_bg, (0, (358 + 7) * h))
59 |
60 | dr = ImageDraw.Draw(im)
61 | sy = DrawText(dr, SIYUAN)
62 | ts = DrawText(dr, TBFONT)
63 | im.alpha_composite(Image.open(maimaidir / 'design.png'), (200, height - 113))
64 | sy.draw(
65 | 700,
66 | height - 70,
67 | 22,
68 | f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} BOT',
69 | ScoreBaseImage.text_color,
70 | 'mm'
71 | )
72 | y = 100
73 | for _lv in lvlist:
74 | x = 160
75 | y += 20
76 | im.alpha_composite(
77 | Image.open(maimaidir / 'UI_CMN_Chara_Level_S_01.png').resize((80, 80)), (50, y + 80)
78 | )
79 | ts.draw(88, y + 120, 35, _lv, anchor='mm')
80 | for num, music in enumerate(lvlist[_lv]):
81 | if num % 14 == 0:
82 | x = 160
83 | y += 85
84 | else:
85 | x += 85
86 | cover = Image.open(music_picture(music.id)).resize((75, 75))
87 | im.alpha_composite(cover, (x, y))
88 | if music.type == 'DX':
89 | im.alpha_composite(dx, (x + 31, y))
90 | im.alpha_composite(diff[int(music.lv)], (x, y + 59))
91 | ts.draw(x + 37, y + 67, 13, music.id, ScoreBaseImage.t_color[int(music.lv)], 'mm')
92 | if not lvlist[_lv]:
93 | y += 85
94 |
95 | by = BytesIO()
96 | im.save(by, 'PNG')
97 | async with aiofiles.open(picname, 'wb') as f:
98 | await f.write(by.getbuffer())
99 | _ntime = int(time.time() - _otime)
100 | atime += _ntime
101 | log.info(f'lv.{lv} 定数表更新完成,耗时:{_ntime}s')
102 | log.info(f'定数表更新完成,共耗时{atime}s')
103 | return f'定数表更新完成,共耗时{atime}s'
104 | except Exception as e:
105 | log.error(traceback.format_exc())
106 | return f'定数表更新失败,Error: {e}'
107 |
108 |
109 | async def update_plate_table() -> str:
110 | """更新完成表"""
111 | try:
112 | version = list(_ for _ in plate_to_version.keys())[1:]
113 | # version.append('霸')
114 | # version.append('舞')
115 | id_bg = Image.new('RGBA', (100, 20), (124, 129, 255, 255))
116 | rlv: Dict[str, List[Music]] = {}
117 | for _ in list(reversed(levelList)):
118 | rlv[_] = []
119 | for _v in version:
120 | if _v in platecn:
121 | _v = platecn[_v]
122 | if _v in ['熊', '华', '華']:
123 | _ver = '熊&华'
124 | elif _v in ['爽', '煌']:
125 | _ver = '爽&煌'
126 | elif _v in ['宙', '星']:
127 | _ver = '宙&星'
128 | elif _v in ['祭', '祝']:
129 | _ver = '祭&祝'
130 | elif _v in ['双', '宴']:
131 | _ver = '双&宴'
132 | else:
133 | _ver = _v
134 |
135 | music_id_list = mai.total_plate_id_list[_ver]
136 | music = mai.total_list.by_id_list(music_id_list)
137 | ralv = copy.deepcopy(rlv)
138 |
139 | for m in music:
140 | ralv[m.level[3]].append(m)
141 |
142 | lines = 0
143 | interval = 0
144 | for _ in ralv:
145 | musicnum = len(ralv[_])
146 | if musicnum == 0:
147 | continue
148 | interval += 1
149 | remainder = musicnum % 10
150 | lines += (musicnum // 10) + (1 if remainder else 0)
151 |
152 | linesheight = 115 * lines + (interval - 1) * 15
153 | """
154 | `linesheight`: 各等级曲绘和间隔总和高度
155 |
156 | - `115` 为曲绘高度 `100` + 间隔 `15`
157 | - `lines` 为行数
158 | - `interval` 为各等级间隔行数
159 | - `(interval - 1) * 15` 为各等级间隔高度,各等级之间间隔为 `30`,所以只加 `15`
160 | """
161 | width, height = 1400, 150 + linesheight + 360
162 | """
163 | `150` 为底部图片 `design` 高度 + 上下间隔高度
164 | `linesheight` 为各等级曲绘和间隔总和高度
165 | `360` 为顶部图片 `` 高度 + 上下间隔高度
166 | """
167 |
168 | im = tricolor_gradient(width, height)
169 |
170 | im.alpha_composite(ScoreBaseImage.aurora_bg)
171 | im.alpha_composite(ScoreBaseImage.shines_bg, (34, 0))
172 | im.alpha_composite(ScoreBaseImage.rainbow_bg, (319, height - 643))
173 | im.alpha_composite(ScoreBaseImage.rainbow_bottom_bg, (100, height - 343))
174 | for h in range((height // 358) + 1):
175 | im.alpha_composite(ScoreBaseImage.pattern_bg, (0, (358 + 7) * h))
176 |
177 | dr = ImageDraw.Draw(im)
178 | ts = DrawText(dr, TBFONT)
179 | sy = DrawText(dr, SIYUAN)
180 | im.alpha_composite(Image.open(maimaidir / 'design.png'), (200, height - 113))
181 | sy.draw(
182 | 700,
183 | height - 70,
184 | 22,
185 | f'Designed by Yuri-YuzuChaN & BlueDeer233. Generated by {BOTNAME} BOT',
186 | ScoreBaseImage.text_color,
187 | 'mm'
188 | )
189 | y = 245
190 | for r in ralv:
191 | if _v in ['霸', '舞']:
192 | ralv[r].sort(key=lambda x: x.ds[-1], reverse=True)
193 | else:
194 | ralv[r].sort(key=lambda x: x.ds[3], reverse=True)
195 | if ralv[r]:
196 | y += 15
197 | im.alpha_composite(
198 | Image.open(maimaidir / 'UI_CMN_Chara_Level_S_01.png'), (65, y + 115)
199 | )
200 | ts.draw(113, y + 164, 35, r, anchor='mm')
201 | x = 200
202 | for num, music in enumerate(ralv[r]):
203 | if num % 10 == 0:
204 | x = 200
205 | y += 115
206 | else:
207 | x += 115
208 | cover = music_picture(music.id)
209 | im.alpha_composite(Image.open(cover).resize((100, 100)), (x, y))
210 | im.alpha_composite(id_bg, (x, y + 80))
211 | ts.draw(x + 50, y + 88, 20, music.id, anchor='mm')
212 |
213 | by = BytesIO()
214 | im.save(by, 'PNG')
215 | async with aiofiles.open(platedir / f'{_v}.png', 'wb') as f:
216 | await f.write(by.getbuffer())
217 | log.info(f'{_v}代牌子更新完成')
218 | return f'完成表更新完成'
219 | except Exception as e:
220 | log.error(traceback.format_exc())
221 | return f'完成表更新失败,Error: {e}'
--------------------------------------------------------------------------------
/libraries/tool.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | import time
4 | from pathlib import Path
5 | from typing import Any, Union
6 |
7 | import aiofiles
8 | from playwright.async_api import async_playwright
9 |
10 | from .. import SNAPSHOT_JS, pie_html_file
11 |
12 |
13 | def qqhash(qq: int):
14 | days = int(time.strftime("%d", time.localtime(time.time()))) + 31 * int(
15 | time.strftime("%m", time.localtime(time.time()))) + 77
16 | return (days * qq) >> 8
17 |
18 |
19 | async def openfile(file: Path) -> Union[dict, list]:
20 | async with aiofiles.open(file, 'r', encoding='utf-8') as f:
21 | data = json.loads(await f.read())
22 | return data
23 |
24 |
25 | async def writefile(file: Path, data: Any) -> bool:
26 | async with aiofiles.open(file, 'w', encoding='utf-8') as f:
27 | await f.write(json.dumps(data, ensure_ascii=False, indent=4))
28 | return True
29 |
30 | async def run_chrome_to_base64() -> str:
31 | async with async_playwright() as p:
32 | browers = await p.chromium.launch(headless=True)
33 | page = await browers.new_page(java_script_enabled=True)
34 | await page.goto('file://' + str(pie_html_file))
35 | await asyncio.sleep(2)
36 |
37 | content: str = await page.evaluate(SNAPSHOT_JS)
38 | await browers.close()
39 |
40 | content_array = content.split(',')
41 | if len(content_array) != 2:
42 | raise OSError(content_array)
43 |
44 | return 'base64://' + content_array[-1]
--------------------------------------------------------------------------------
/maimai.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_startup
2 |
3 | from .command import *
4 | from .libraries.maimai_best_50 import ScoreBaseImage
5 | from .libraries.maimaidx_api_data import maiApi
6 | from .libraries.maimaidx_music import mai
7 |
8 |
9 | @on_startup
10 | async def _():
11 | """
12 | bot启动时开始获取所有数据
13 | """
14 | if maiApi.config.maimaidxproberproxy:
15 | log.info('正在使用代理服务器访问查分器')
16 | if maiApi.config.maimaidxaliasproxy:
17 | log.info('正在使用代理服务器访问别名服务器')
18 | maiApi.load_token_proxy()
19 | log.info('正在获取maimai所有曲目信息')
20 | await mai.get_music()
21 | log.info('正在获取maimai牌子数据')
22 | await mai.get_plate_json()
23 | log.info('正在获取maimai所有曲目别名信息')
24 | await mai.get_music_alias()
25 | mai.guess()
26 | log.info('maimai数据获取完成')
27 |
28 | if maiApi.config.saveinmem:
29 | ScoreBaseImage._load_image()
30 | log.info('已将图片保存在内存中')
31 |
32 | if not list(ratingdir.iterdir()):
33 | log.warning(
34 | '