nonebot-plugin-osubot 6.22.3__py3-none-any.whl → 6.26.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_osubot/api.py +12 -7
- nonebot_plugin_osubot/config.py +10 -10
- nonebot_plugin_osubot/draw/bmap.py +20 -22
- nonebot_plugin_osubot/draw/bp.py +3 -13
- nonebot_plugin_osubot/draw/catch_preview.py +2 -16
- nonebot_plugin_osubot/draw/echarts.py +8 -1
- nonebot_plugin_osubot/draw/info.py +59 -207
- nonebot_plugin_osubot/draw/info_templates/index.html +507 -0
- nonebot_plugin_osubot/draw/info_templates/output.css +2 -0
- nonebot_plugin_osubot/draw/map.py +9 -11
- nonebot_plugin_osubot/draw/osu_preview.py +50 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/css/style.css +258 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/README.md +109 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js +3 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js.map +1 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js +3 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js.map +1 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/beatmap.js +211 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/hitobject.js +29 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/point.js +55 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/scroll.js +45 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/timingpoint.js +35 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/LegacyRandom.js +81 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/PalpableCatchHitObject.js +53 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/bananashower.js +33 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/catch.js +211 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/fruit.js +21 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/juicestream.js +176 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/hitnote.js +21 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/holdnote.js +37 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/mania.js +164 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/preview.js +61 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/bezier2.js +33 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/catmullcurve.js +34 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/centripetalcatmullrom.js +30 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/circumstancedcircle.js +47 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curve.js +25 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curvetype.js +17 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/equaldistancemulticurve.js +70 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/linearbezier.js +40 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/hitcircle.js +85 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/slider.js +120 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/spinner.js +56 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/standard.js +170 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/donkat.js +40 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/drumroll.js +34 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/shaker.js +58 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/taiko.js +120 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/js/util.js +61 -0
- nonebot_plugin_osubot/draw/osu_preview_templates/pic.html +110 -0
- nonebot_plugin_osubot/draw/rating.py +6 -3
- nonebot_plugin_osubot/draw/score.py +23 -81
- nonebot_plugin_osubot/draw/taiko_preview.py +14 -13
- nonebot_plugin_osubot/draw/templates/bpa_chart.html +1 -1
- nonebot_plugin_osubot/draw/utils.py +162 -16
- nonebot_plugin_osubot/file.py +184 -31
- nonebot_plugin_osubot/mania/__init__.py +9 -10
- nonebot_plugin_osubot/matcher/__init__.py +2 -0
- nonebot_plugin_osubot/matcher/bp_analyze.py +14 -9
- nonebot_plugin_osubot/matcher/guess.py +250 -294
- nonebot_plugin_osubot/matcher/map_convert.py +21 -13
- nonebot_plugin_osubot/matcher/medal.py +1 -1
- nonebot_plugin_osubot/matcher/osudl.py +5 -4
- nonebot_plugin_osubot/matcher/pr.py +0 -4
- nonebot_plugin_osubot/matcher/preview.py +10 -3
- nonebot_plugin_osubot/matcher/recommend.py +7 -12
- nonebot_plugin_osubot/mods.py +62 -61
- nonebot_plugin_osubot/network/first_response.py +1 -1
- nonebot_plugin_osubot/osufile/mods/AP.png +0 -0
- nonebot_plugin_osubot/osufile/mods/CL.png +0 -0
- nonebot_plugin_osubot/osufile/mods/DT.png +0 -0
- nonebot_plugin_osubot/osufile/mods/EZ.png +0 -0
- nonebot_plugin_osubot/osufile/mods/FI.png +0 -0
- nonebot_plugin_osubot/osufile/mods/FL.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HR.png +0 -0
- nonebot_plugin_osubot/osufile/mods/HT.png +0 -0
- nonebot_plugin_osubot/osufile/mods/MR.png +0 -0
- nonebot_plugin_osubot/osufile/mods/NC.png +0 -0
- nonebot_plugin_osubot/osufile/mods/NF.png +0 -0
- nonebot_plugin_osubot/osufile/mods/PF.png +0 -0
- nonebot_plugin_osubot/osufile/mods/RX.png +0 -0
- nonebot_plugin_osubot/osufile/mods/SD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/SO.png +0 -0
- nonebot_plugin_osubot/osufile/mods/TD.png +0 -0
- nonebot_plugin_osubot/osufile/mods/V2.png +0 -0
- nonebot_plugin_osubot/pp.py +7 -0
- nonebot_plugin_osubot/schema/__init__.py +0 -2
- nonebot_plugin_osubot/schema/beatmapsets.py +42 -0
- nonebot_plugin_osubot/schema/draw_info.py +54 -0
- nonebot_plugin_osubot/schema/score.py +1 -0
- nonebot_plugin_osubot/schema/user.py +1 -0
- {nonebot_plugin_osubot-6.22.3.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/METADATA +18 -17
- {nonebot_plugin_osubot-6.22.3.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/RECORD +95 -55
- nonebot_plugin_osubot-6.26.4.dist-info/WHEEL +4 -0
- nonebot_plugin_osubot/data/osu/1849145.osz +0 -0
- nonebot_plugin_osubot/data/osu/1849145.zip +0 -0
- nonebot_plugin_osubot/draw/templates/t.html +0 -58
- nonebot_plugin_osubot/schema/sayo_beatmap.py +0 -59
- nonebot_plugin_osubot-6.22.3.dist-info/WHEEL +0 -4
nonebot_plugin_osubot/api.py
CHANGED
|
@@ -11,19 +11,19 @@ from nonebot import get_plugin_config
|
|
|
11
11
|
from httpx import Response
|
|
12
12
|
|
|
13
13
|
from .network.manager import network_manager
|
|
14
|
+
from .schema.beatmapsets import BeatmapSets
|
|
14
15
|
from .utils import FGM
|
|
15
16
|
from .config import Config
|
|
16
17
|
from .mods import get_mods
|
|
17
18
|
from .network import auto_retry
|
|
18
19
|
from .exceptions import NetworkError
|
|
19
20
|
from .network.first_response import get_first_response
|
|
20
|
-
from .schema import User, NewScore,
|
|
21
|
+
from .schema import User, NewScore, RecommendData
|
|
21
22
|
from .schema.score import UnifiedScore, NewStatistics, UnifiedBeatmap
|
|
22
23
|
from .schema.ppysb import InfoResponse, ScoresResponse, V2ScoresResponse
|
|
23
24
|
from .schema.user import Level, GradeCounts, UnifiedUser, UserStatistics
|
|
24
25
|
|
|
25
26
|
api = "https://osu.ppy.sh/api/v2"
|
|
26
|
-
sayoapi = "https://api.sayobot.cn"
|
|
27
27
|
cache = ExpiringDict(max_len=1, max_age_seconds=86400)
|
|
28
28
|
plugin_config = get_plugin_config(Config)
|
|
29
29
|
|
|
@@ -122,6 +122,7 @@ async def fetch_score_batch(
|
|
|
122
122
|
hp=i.beatmap.drain,
|
|
123
123
|
od=i.beatmap.accuracy,
|
|
124
124
|
stars=i.beatmap.difficulty_rating,
|
|
125
|
+
convert=i.beatmap.convert,
|
|
125
126
|
),
|
|
126
127
|
beatmapset=i.beatmapset,
|
|
127
128
|
)
|
|
@@ -189,6 +190,7 @@ async def get_user_scores(
|
|
|
189
190
|
ended_at=datetime.strptime(i.play_time, "%Y-%m-%dT%H:%M:%S") + timedelta(hours=8),
|
|
190
191
|
max_combo=i.max_combo,
|
|
191
192
|
passed=True,
|
|
193
|
+
pp=i.pp,
|
|
192
194
|
statistics=NewStatistics(
|
|
193
195
|
miss=i.nmiss,
|
|
194
196
|
perfect=i.ngeki,
|
|
@@ -412,12 +414,13 @@ async def get_random_bg() -> Optional[bytes]:
|
|
|
412
414
|
return res.content
|
|
413
415
|
|
|
414
416
|
|
|
415
|
-
async def
|
|
416
|
-
|
|
417
|
-
|
|
417
|
+
async def get_beatmapsets_info(sid) -> BeatmapSets:
|
|
418
|
+
url = f"https://osu.ppy.sh/api/v2/beatmapsets/{sid}"
|
|
419
|
+
res = await make_request(url, await get_headers(), "未查询到该谱面集(Setid)信息")
|
|
420
|
+
return BeatmapSets(**res)
|
|
418
421
|
|
|
419
422
|
|
|
420
|
-
async def get_map_bg(mapid, sid, bg_name) -> BytesIO:
|
|
423
|
+
async def get_map_bg(mapid, sid, bg_name) -> BytesIO | None:
|
|
421
424
|
res = await get_first_response(
|
|
422
425
|
[
|
|
423
426
|
f"https://catboy.best/preview/background/{mapid}",
|
|
@@ -425,7 +428,9 @@ async def get_map_bg(mapid, sid, bg_name) -> BytesIO:
|
|
|
425
428
|
f"https://dl.sayobot.cn/beatmaps/files/{sid}/{bg_name}",
|
|
426
429
|
]
|
|
427
430
|
)
|
|
428
|
-
|
|
431
|
+
if res:
|
|
432
|
+
return BytesIO(res.content)
|
|
433
|
+
return None
|
|
429
434
|
|
|
430
435
|
|
|
431
436
|
async def get_seasonal_bg() -> Optional[dict]:
|
nonebot_plugin_osubot/config.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Union, Optional
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Config(BaseModel):
|
|
7
|
-
osu_client: Optional[int] = None
|
|
8
|
-
osu_key: Optional[str] = None
|
|
9
|
-
info_bg: Optional[list[str]] = ["https://t.alcy.cc/mp", "https://t.alcy.cc/moemp"]
|
|
10
|
-
osu_proxy: Optional[Union[str, dict]] = None
|
|
1
|
+
from typing import Union, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config(BaseModel):
|
|
7
|
+
osu_client: Optional[int] = None
|
|
8
|
+
osu_key: Optional[str] = None
|
|
9
|
+
info_bg: Optional[list[str]] = ["https://t.alcy.cc/mp", "https://t.alcy.cc/moemp"]
|
|
10
|
+
osu_proxy: Optional[Union[str, dict]] = None
|
|
@@ -4,25 +4,20 @@ from datetime import datetime, timedelta
|
|
|
4
4
|
from PIL import ImageDraw, ImageFilter, ImageEnhance
|
|
5
5
|
|
|
6
6
|
from ..file import get_projectimg
|
|
7
|
-
from ..api import
|
|
8
|
-
from ..exceptions import NetworkError
|
|
7
|
+
from ..api import get_beatmapsets_info
|
|
9
8
|
from .utils import crop_bg, stars_diff, calc_songlen
|
|
10
|
-
from .static import Image, BarImg, IconLs, Torus_SemiBold_20, Torus_SemiBold_40, Torus_SemiBold_50, extra_30
|
|
9
|
+
from .static import Image, BarImg, IconLs, Torus_SemiBold_20, Torus_SemiBold_40, Torus_SemiBold_50, extra_30, Stars
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
async def draw_bmap_info(mapid) -> BytesIO:
|
|
14
|
-
|
|
15
|
-
if sayo_info.status == -1:
|
|
16
|
-
raise NetworkError("在sayobot未查询到该地图")
|
|
17
|
-
data = sayo_info.data
|
|
18
|
-
|
|
13
|
+
data = await get_beatmapsets_info(mapid)
|
|
19
14
|
coverurl = f"https://assets.ppy.sh/beatmaps/{mapid}/covers/cover@2x.jpg"
|
|
20
15
|
cover = await get_projectimg(coverurl)
|
|
21
16
|
# 新建
|
|
22
|
-
if len(data.
|
|
17
|
+
if len(data.beatmaps) > 20:
|
|
23
18
|
im_h = 400 + 102 * 20
|
|
24
19
|
else:
|
|
25
|
-
im_h = 400 + 102 * (len(data.
|
|
20
|
+
im_h = 400 + 102 * (len(data.beatmaps) - 1)
|
|
26
21
|
im = Image.new("RGBA", (1200, im_h), (31, 41, 46, 255))
|
|
27
22
|
draw = ImageDraw.Draw(im)
|
|
28
23
|
# 背景
|
|
@@ -37,35 +32,36 @@ async def draw_bmap_info(mapid) -> BytesIO:
|
|
|
37
32
|
# mapper
|
|
38
33
|
draw.text((25, 105), f"谱面作者: {data.creator}", font=Torus_SemiBold_20, anchor="lt")
|
|
39
34
|
# rank时间
|
|
40
|
-
if data.
|
|
35
|
+
if not data.ranked_date:
|
|
41
36
|
approved_date = "谱面状态可能非ranked"
|
|
42
37
|
else:
|
|
43
|
-
datearray = datetime.
|
|
38
|
+
datearray = datetime.fromisoformat(data.ranked_date.replace("Z", ""))
|
|
44
39
|
approved_date = (datearray + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")
|
|
45
40
|
draw.text((25, 140), f"上架时间: {approved_date}", font=Torus_SemiBold_20, anchor="lt")
|
|
46
41
|
# 来源
|
|
47
|
-
|
|
42
|
+
if data.source:
|
|
43
|
+
draw.text((25, 175), f"Source: {data.source}", font=Torus_SemiBold_20, anchor="lt")
|
|
48
44
|
# bpm
|
|
49
45
|
draw.text((1150, 105), f"BPM: {data.bpm}", font=Torus_SemiBold_20, anchor="rt")
|
|
50
46
|
# 曲长
|
|
51
|
-
music_len = calc_songlen(data.
|
|
47
|
+
music_len = calc_songlen(data.beatmaps[0].total_length)
|
|
52
48
|
draw.text((1150, 140), f"length: {music_len}", font=Torus_SemiBold_20, anchor="rt")
|
|
53
49
|
# Setid
|
|
54
50
|
draw.text((1150, 35), f"Setid: {mapid}", font=Torus_SemiBold_20, anchor="rt")
|
|
55
|
-
gmap = sorted(data.
|
|
51
|
+
gmap = sorted(data.beatmaps, key=lambda k: k.difficulty_rating, reverse=False)
|
|
56
52
|
for num, cmap in enumerate(gmap):
|
|
57
53
|
if num < 20:
|
|
58
54
|
h_num = 102 * num
|
|
59
55
|
# 难度
|
|
60
|
-
draw.text((20, 320 + h_num), IconLs[cmap.
|
|
56
|
+
draw.text((20, 320 + h_num), IconLs[cmap.mode_int], font=extra_30, anchor="lt")
|
|
61
57
|
# 星星
|
|
62
|
-
stars_bg = stars_diff(cmap.
|
|
58
|
+
stars_bg = stars_diff(cmap.difficulty_rating, Stars)
|
|
63
59
|
stars_img = stars_bg.resize((80, 30))
|
|
64
60
|
im.alpha_composite(stars_img, (60, 320 + h_num))
|
|
65
61
|
# diff
|
|
66
62
|
im.alpha_composite(BarImg, (10, 365 + h_num))
|
|
67
63
|
gc = ["CS", "HP", "OD", "AR"]
|
|
68
|
-
for index, i in enumerate((cmap.
|
|
64
|
+
for index, i in enumerate((cmap.cs, cmap.drain, cmap.accuracy, cmap.ar)):
|
|
69
65
|
diff_len = int(200 * i / 10) if i <= 10 else 200
|
|
70
66
|
diff_bg = Image.new("RGBA", (diff_len, 12), (255, 255, 255, 255))
|
|
71
67
|
im.alpha_composite(diff_bg, (50 + 300 * index, 365 + h_num))
|
|
@@ -90,11 +86,13 @@ async def draw_bmap_info(mapid) -> BytesIO:
|
|
|
90
86
|
anchor="lm",
|
|
91
87
|
)
|
|
92
88
|
# 难度
|
|
93
|
-
if cmap.
|
|
89
|
+
if cmap.difficulty_rating < 6.5:
|
|
94
90
|
color = (0, 0, 0, 255)
|
|
95
91
|
else:
|
|
96
92
|
color = (255, 217, 102, 255)
|
|
97
|
-
draw.text(
|
|
93
|
+
draw.text(
|
|
94
|
+
(65, 335 + h_num), f"★{cmap.difficulty_rating:.2f}", font=Torus_SemiBold_20, anchor="lm", fill=color
|
|
95
|
+
)
|
|
98
96
|
# version
|
|
99
97
|
draw.text(
|
|
100
98
|
(150, 335 + h_num),
|
|
@@ -105,14 +103,14 @@ async def draw_bmap_info(mapid) -> BytesIO:
|
|
|
105
103
|
# mapid
|
|
106
104
|
draw.text(
|
|
107
105
|
(1150, 328 + h_num),
|
|
108
|
-
f"Mapid: {cmap.
|
|
106
|
+
f"Mapid: {cmap.id}",
|
|
109
107
|
font=Torus_SemiBold_20,
|
|
110
108
|
anchor="rm",
|
|
111
109
|
)
|
|
112
110
|
# maxcb
|
|
113
111
|
draw.text(
|
|
114
112
|
(700, 328 + h_num),
|
|
115
|
-
f"Max Combo: {cmap.
|
|
113
|
+
f"Max Combo: {cmap.max_combo or 0}",
|
|
116
114
|
font=Torus_SemiBold_20,
|
|
117
115
|
anchor="lm",
|
|
118
116
|
)
|
nonebot_plugin_osubot/draw/bp.py
CHANGED
|
@@ -9,7 +9,7 @@ from ..pp import cal_pp
|
|
|
9
9
|
from ..mods import get_mods_list
|
|
10
10
|
from ..exceptions import NetworkError
|
|
11
11
|
from ..schema.score import Mod, UnifiedScore
|
|
12
|
-
from .score import
|
|
12
|
+
from .score import cal_score_info
|
|
13
13
|
from ..api import get_user_scores, get_user_info_data
|
|
14
14
|
from ..file import map_path, get_pfm_img, download_osu
|
|
15
15
|
from .utils import draw_fillet, draw_fillet2, open_user_icon, filter_scores_with_regex
|
|
@@ -29,8 +29,6 @@ async def draw_bp(
|
|
|
29
29
|
source: str,
|
|
30
30
|
) -> BytesIO:
|
|
31
31
|
scores = await get_user_scores(uid, mode, "best", source=source, legacy_only=not is_lazer)
|
|
32
|
-
if not is_lazer:
|
|
33
|
-
scores = [i for i in scores if any(mod.acronym == "CL" for mod in i.mods)]
|
|
34
32
|
if mods:
|
|
35
33
|
mods_ls = get_mods_list(scores, mods)
|
|
36
34
|
if low_bound > len(mods_ls):
|
|
@@ -46,17 +44,9 @@ async def draw_bp(
|
|
|
46
44
|
score_ls_filtered = [score for score in scores if score.ended_at > datetime.now() - timedelta(days=day + 1)]
|
|
47
45
|
if not score_ls_filtered:
|
|
48
46
|
raise NetworkError("未查询到游玩记录")
|
|
49
|
-
for score_info in score_ls_filtered:
|
|
47
|
+
for i, score_info in enumerate(score_ls_filtered):
|
|
50
48
|
# 判断是否开启lazer模式
|
|
51
|
-
|
|
52
|
-
score_info.legacy_total_score = score_info.total_score
|
|
53
|
-
if not is_lazer and Mod(acronym="CL") in score_info.mods:
|
|
54
|
-
score_info.mods.remove(Mod(acronym="CL"))
|
|
55
|
-
if score_info.ruleset_id == 3 and not is_lazer:
|
|
56
|
-
score_info.accuracy = cal_legacy_acc(score_info.statistics)
|
|
57
|
-
if not is_lazer:
|
|
58
|
-
is_hidden = any(i in score_info.mods for i in (Mod(acronym="HD"), Mod(acronym="FL"), Mod(acronym="FI")))
|
|
59
|
-
score_info.rank = cal_legacy_rank(score_info, is_hidden)
|
|
49
|
+
score_ls_filtered[i] = cal_score_info(is_lazer, score_info, source)
|
|
60
50
|
if search_condition:
|
|
61
51
|
score_ls_filtered = filter_scores_with_regex(score_ls_filtered, search_condition)
|
|
62
52
|
if not score_ls_filtered:
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
import jinja2
|
|
4
3
|
from nonebot_plugin_htmlrender import get_new_page
|
|
5
4
|
|
|
6
|
-
from
|
|
5
|
+
from .utils import load_osu_file_and_setup_template
|
|
7
6
|
|
|
8
7
|
template_path = str(Path(__file__).parent / "catch_preview_templates")
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
async def draw_cath_preview(beatmap_id, beatmapset_id, mods) -> bytes:
|
|
12
|
-
|
|
13
|
-
if not path.exists():
|
|
14
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
15
|
-
osu = path / f"{beatmap_id}.osu"
|
|
16
|
-
if not osu.exists():
|
|
17
|
-
await download_osu(beatmapset_id, beatmap_id)
|
|
18
|
-
with open(osu, encoding="utf-8-sig") as f:
|
|
19
|
-
osu_file = f.read()
|
|
20
|
-
template_name = "pic.html"
|
|
21
|
-
template_env = jinja2.Environment( # noqa: S701
|
|
22
|
-
loader=jinja2.FileSystemLoader(template_path),
|
|
23
|
-
enable_async=True,
|
|
24
|
-
)
|
|
25
|
-
template = template_env.get_template(template_name)
|
|
11
|
+
osu_file, template = await load_osu_file_and_setup_template(template_path, beatmap_id, beatmapset_id)
|
|
26
12
|
is_hr = 1 if "HR" in mods else 0
|
|
27
13
|
is_ez = 1 if "EZ" in mods else 0
|
|
28
14
|
is_dt = 1 if "DT" in mods else 0
|
|
@@ -20,7 +20,14 @@ async def draw_bpa_plot(name, pp_ls, length_ls, mod_pp_ls, mapper_pp_ls) -> byte
|
|
|
20
20
|
pic = await template_to_pic(
|
|
21
21
|
template_path,
|
|
22
22
|
template_name,
|
|
23
|
-
{
|
|
23
|
+
{
|
|
24
|
+
"name": name,
|
|
25
|
+
"pp_ls": pp_ls,
|
|
26
|
+
"length_ls": length_ls,
|
|
27
|
+
"mod_pp_ls": mod_pp_ls,
|
|
28
|
+
"mapper_pp_ls": mapper_pp_ls,
|
|
29
|
+
"length": len(pp_ls),
|
|
30
|
+
},
|
|
24
31
|
)
|
|
25
32
|
return pic
|
|
26
33
|
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import base64
|
|
2
|
+
import jinja2
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Union
|
|
4
5
|
from datetime import date, datetime, timedelta
|
|
5
6
|
|
|
6
|
-
from PIL import
|
|
7
|
+
from PIL import UnidentifiedImageError
|
|
8
|
+
from nonebot_plugin_htmlrender import get_new_page
|
|
7
9
|
|
|
10
|
+
from .utils import info_calc
|
|
8
11
|
from ..utils import FGM, GMN
|
|
12
|
+
from ..file import user_cache_path
|
|
9
13
|
from ..exceptions import NetworkError
|
|
10
14
|
from ..database.models import InfoData
|
|
15
|
+
from ..schema.draw_info import DrawUser, Badge
|
|
11
16
|
from ..api import get_random_bg, get_user_info_data
|
|
12
|
-
from .utils import info_calc, draw_fillet, update_icon, open_user_icon
|
|
13
|
-
from ..file import get_projectimg, team_cache_path, user_cache_path, badge_cache_path, make_badge_cache_file
|
|
14
|
-
from .static import (
|
|
15
|
-
Image,
|
|
16
|
-
InfoImg,
|
|
17
|
-
ExpLeftBg,
|
|
18
|
-
ExpRightBg,
|
|
19
|
-
ExpCenterBg,
|
|
20
|
-
Torus_Regular_20,
|
|
21
|
-
Torus_Regular_25,
|
|
22
|
-
Torus_Regular_30,
|
|
23
|
-
Torus_Regular_35,
|
|
24
|
-
Torus_Regular_40,
|
|
25
|
-
Torus_Regular_45,
|
|
26
|
-
Torus_Regular_50,
|
|
27
|
-
osufile,
|
|
28
|
-
)
|
|
29
17
|
|
|
30
18
|
|
|
31
|
-
async def draw_info(uid: Union[int, str], mode: str, day: int, source: str) ->
|
|
19
|
+
async def draw_info(uid: Union[int, str], mode: str, day: int, source: str) -> bytes:
|
|
32
20
|
info = await get_user_info_data(uid, mode, source)
|
|
33
21
|
statistics = info.statistics
|
|
34
22
|
if statistics.play_count == 0:
|
|
@@ -69,208 +57,72 @@ async def draw_info(uid: Union[int, str], mode: str, day: int, source: str) -> B
|
|
|
69
57
|
statistics.play_count,
|
|
70
58
|
statistics.total_hits,
|
|
71
59
|
)
|
|
72
|
-
# 新建
|
|
73
|
-
im = Image.new("RGBA", (1000, 1350))
|
|
74
|
-
draw = ImageDraw.Draw(im)
|
|
75
60
|
# 获取背景
|
|
76
61
|
bg_path = user_cache_path / str(info.id) / "info.png"
|
|
77
62
|
if bg_path.exists():
|
|
78
63
|
try:
|
|
79
|
-
|
|
64
|
+
with open(bg_path, "rb") as image_file:
|
|
65
|
+
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
|
66
|
+
|
|
67
|
+
# 格式化为 CSS 接受的 data URI 格式
|
|
68
|
+
bg = f"data:image/png;base64,{encoded_string}"
|
|
80
69
|
except UnidentifiedImageError:
|
|
81
70
|
bg_path.unlink()
|
|
82
71
|
raise NetworkError("自定义背景图片读取错误,请重新上传!")
|
|
83
72
|
else:
|
|
84
73
|
bg = await get_random_bg()
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
bg_ratio = height / width
|
|
91
|
-
ratio = 1350 / 1000
|
|
92
|
-
if bg_ratio > ratio:
|
|
93
|
-
height = ratio * width
|
|
94
|
-
else:
|
|
95
|
-
width = height / ratio
|
|
96
|
-
x, y = bg.size
|
|
97
|
-
x, y = (x - width) // 2, (y - height) // 2
|
|
98
|
-
bg = bg.crop((x, y, x + width, y + height)).resize((1000, 1350))
|
|
99
|
-
im.alpha_composite(bg, (0, 0))
|
|
100
|
-
# 获取头图,头像,地区,状态,supporter
|
|
101
|
-
path = user_cache_path / str(info.id)
|
|
102
|
-
if not path.exists():
|
|
103
|
-
path.mkdir()
|
|
104
|
-
country = osufile / "flags" / f"{info.country_code}.png"
|
|
105
|
-
# 底图
|
|
106
|
-
im.alpha_composite(InfoImg)
|
|
107
|
-
# 4/7K排名
|
|
108
|
-
if FGM[mode] == 3 and statistics.variants:
|
|
109
|
-
for variant in statistics.variants:
|
|
110
|
-
if variant.variant == "4k" and variant.pp != 0:
|
|
111
|
-
draw.text(
|
|
112
|
-
(935, 85),
|
|
113
|
-
f"4K: {variant.pp} // #{variant.global_rank} // {info.country_code} #{variant.country_rank}",
|
|
114
|
-
font=Torus_Regular_25,
|
|
115
|
-
anchor="rt",
|
|
116
|
-
)
|
|
117
|
-
if variant.variant == "7k" and variant.pp != 0:
|
|
118
|
-
draw.text(
|
|
119
|
-
(935, 120),
|
|
120
|
-
f"7K: {variant.pp} // #{variant.global_rank} // {info.country_code} #{variant.country_rank}",
|
|
121
|
-
font=Torus_Regular_25,
|
|
122
|
-
anchor="rt",
|
|
123
|
-
)
|
|
124
|
-
# 奖牌
|
|
125
|
-
if info.badges:
|
|
126
|
-
badges_num = len(info.badges)
|
|
127
|
-
for num, badge in enumerate(info.badges):
|
|
128
|
-
if badges_num <= 9:
|
|
129
|
-
length = 50 + 100 * num
|
|
130
|
-
height = 510
|
|
131
|
-
elif num < 9:
|
|
132
|
-
length = 50 + 100 * num
|
|
133
|
-
height = 486
|
|
134
|
-
else:
|
|
135
|
-
length = 50 + 100 * (num - 9)
|
|
136
|
-
height = 534
|
|
137
|
-
badges_path = badge_cache_path / f"{hash(badge.description)}.png"
|
|
138
|
-
if not badges_path.exists():
|
|
139
|
-
await make_badge_cache_file(badge)
|
|
140
|
-
try:
|
|
141
|
-
badges_img = Image.open(badges_path).convert("RGBA").resize((86, 40))
|
|
142
|
-
except UnidentifiedImageError:
|
|
143
|
-
badges_path.unlink()
|
|
144
|
-
raise NetworkError("badges 图片下载错误,请重试!")
|
|
145
|
-
im.alpha_composite(badges_img, (length, height))
|
|
146
|
-
# 地区
|
|
147
|
-
country_bg = Image.open(country).convert("RGBA").resize((80, 54))
|
|
148
|
-
im.alpha_composite(country_bg, (400, 394))
|
|
149
|
-
if info.team and info.team.flag_url:
|
|
150
|
-
team_path = team_cache_path / f"{info.team.id}.png"
|
|
151
|
-
if not team_path.exists():
|
|
152
|
-
team_img = await get_projectimg(info.team.flag_url)
|
|
153
|
-
team_img = Image.open(team_img).convert("RGBA")
|
|
154
|
-
team_img.save(team_path)
|
|
155
|
-
try:
|
|
156
|
-
team_img = Image.open(team_path).convert("RGBA").resize((108, 54))
|
|
157
|
-
im.alpha_composite(team_img, (400, 280))
|
|
158
|
-
except UnidentifiedImageError:
|
|
159
|
-
team_path.unlink()
|
|
160
|
-
raise NetworkError("team 图片下载错误,请重试!")
|
|
161
|
-
draw.text((515, 300), info.team.name, font=Torus_Regular_30, anchor="lt")
|
|
162
|
-
# supporter
|
|
163
|
-
# if info.is_supporter:
|
|
164
|
-
# im.alpha_composite(SupporterBg.resize((54, 54)), (400, 280))
|
|
165
|
-
# 经验
|
|
166
|
-
if statistics.level.progress != 0:
|
|
167
|
-
im.alpha_composite(ExpLeftBg, (50, 646))
|
|
168
|
-
exp_width = statistics.level.progress * 7 - 3
|
|
169
|
-
im.alpha_composite(ExpCenterBg.resize((exp_width, 10)), (54, 646))
|
|
170
|
-
im.alpha_composite(ExpRightBg, (int(54 + exp_width), 646))
|
|
171
|
-
# 模式
|
|
172
|
-
draw.text((935, 50), GMN[mode], font=Torus_Regular_45, anchor="rm")
|
|
173
|
-
# 玩家名
|
|
174
|
-
draw.text((400, 205), info.username, font=Torus_Regular_50, anchor="lm")
|
|
175
|
-
# 地区排名
|
|
176
|
-
op, value = info_calc(statistics.country_rank, n_crank, rank=True)
|
|
177
|
-
if not statistics.country_rank:
|
|
178
|
-
t_crank = "#0"
|
|
179
|
-
else:
|
|
180
|
-
t_crank = f"#{statistics.country_rank:,}({op}{value:,})" if value != 0 else f"#{statistics.country_rank:,}"
|
|
181
|
-
draw.text((495, 448), t_crank, font=Torus_Regular_30, anchor="lb")
|
|
182
|
-
# 等级
|
|
183
|
-
draw.text((900, 650), str(statistics.level.current), font=Torus_Regular_25, anchor="mm")
|
|
184
|
-
# 经验百分比
|
|
185
|
-
draw.text((750, 660), f"{statistics.level.progress}%", font=Torus_Regular_20, anchor="rt")
|
|
186
|
-
# 全球排名
|
|
187
|
-
if not statistics.global_rank:
|
|
188
|
-
draw.text((55, 785), "#0", font=Torus_Regular_35, anchor="lt")
|
|
74
|
+
if day != 0 and user:
|
|
75
|
+
day_delta = date.today() - user.date
|
|
76
|
+
time = day_delta.days
|
|
77
|
+
footer = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
78
|
+
footer += f" | 数据对比于 {time} 天前"
|
|
189
79
|
else:
|
|
190
|
-
|
|
191
|
-
(55, 785),
|
|
192
|
-
f"#{statistics.global_rank:,}",
|
|
193
|
-
font=Torus_Regular_35,
|
|
194
|
-
anchor="lt",
|
|
195
|
-
)
|
|
196
|
-
op, value = info_calc(statistics.global_rank, n_grank, rank=True)
|
|
197
|
-
if value != 0:
|
|
198
|
-
draw.text((65, 820), f"{op}{value:,}", font=Torus_Regular_20, anchor="lt")
|
|
199
|
-
# pp
|
|
200
|
-
draw.text((295, 785), f"{statistics.pp:,}", font=Torus_Regular_35, anchor="lt")
|
|
80
|
+
footer = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
201
81
|
op, value = info_calc(statistics.pp, n_pp, pp=True)
|
|
202
|
-
if value != 0
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
draw.text((493 + 100 * gc_num, 788), f"{num}", font=Torus_Regular_30, anchor="mt")
|
|
208
|
-
# gc_x+=100
|
|
209
|
-
# rank分
|
|
210
|
-
draw.text((935, 895), f"{statistics.ranked_score:,}", font=Torus_Regular_40, anchor="rt")
|
|
82
|
+
pp_change = f"{op}{value:,.2f}" if value != 0 else None
|
|
83
|
+
op, value = info_calc(statistics.global_rank, n_grank, rank=True)
|
|
84
|
+
rank_change = f"{op}{value:,}" if value != 0 else None
|
|
85
|
+
op, value = info_calc(statistics.country_rank, n_crank, rank=True)
|
|
86
|
+
country_rank_change = f"({op}{value:,})" if value != 0 else None
|
|
211
87
|
# acc
|
|
212
88
|
op, value = info_calc(statistics.hit_accuracy, n_acc)
|
|
213
|
-
|
|
214
|
-
draw.text((935, 965), t_acc, font=Torus_Regular_40, anchor="rt")
|
|
89
|
+
acc_change = f"({op}{value:.2f}%)" if value != 0 else None
|
|
215
90
|
# 游玩次数
|
|
216
91
|
op, value = info_calc(statistics.play_count, n_pc)
|
|
217
|
-
|
|
218
|
-
draw.text((935, 1035), t_pc, font=Torus_Regular_40, anchor="rt")
|
|
92
|
+
pc_change = f"({op}{value:,})" if value != 0 else None
|
|
219
93
|
# 总分
|
|
220
|
-
draw.text((935, 1105), f"{statistics.total_score:,}", font=Torus_Regular_40, anchor="rt")
|
|
221
94
|
# 总命中
|
|
222
95
|
op, value = info_calc(statistics.total_hits, n_count)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
draw.text((380, 1305), current_time, font=Torus_Regular_25, anchor="la")
|
|
241
|
-
# 头像
|
|
242
|
-
gif_frames = []
|
|
243
|
-
user_icon = await open_user_icon(info, source)
|
|
244
|
-
_ = asyncio.create_task(update_icon(info))
|
|
245
|
-
if not getattr(user_icon, "is_animated", False):
|
|
246
|
-
icon_bg = user_icon.convert("RGBA").resize((300, 300))
|
|
247
|
-
icon_img = draw_fillet(icon_bg, 25)
|
|
248
|
-
im.alpha_composite(icon_img, (50, 148))
|
|
249
|
-
byt = BytesIO()
|
|
250
|
-
im.convert("RGB").save(byt, "jpeg")
|
|
251
|
-
im.close()
|
|
252
|
-
user_icon.close()
|
|
253
|
-
return byt
|
|
254
|
-
for gif_frame in ImageSequence.Iterator(user_icon):
|
|
255
|
-
# 将 GIF 图片中的每一帧转换为 RGBA 模式
|
|
256
|
-
gif_frame = gif_frame.convert("RGBA").resize((300, 300))
|
|
257
|
-
gif_frame = draw_fillet(gif_frame, 25)
|
|
258
|
-
# 创建一个新的 RGBA 图片,将 PNG 图片作为背景,将当前帧添加到背景上
|
|
259
|
-
rgba_frame = Image.new("RGBA", im.size, (0, 0, 0, 0))
|
|
260
|
-
rgba_frame.paste(im, (0, 0), im)
|
|
261
|
-
rgba_frame.paste(gif_frame, (50, 148), gif_frame)
|
|
262
|
-
# 将 RGBA 图片转换为 RGB 模式,并添加到 GIF 图片中
|
|
263
|
-
gif_frames.append(rgba_frame)
|
|
264
|
-
gif_bytes = BytesIO()
|
|
265
|
-
# 保存 GIF 图片
|
|
266
|
-
gif_frames[0].save(
|
|
267
|
-
gif_bytes,
|
|
268
|
-
format="gif",
|
|
269
|
-
save_all=True,
|
|
270
|
-
append_images=gif_frames[1:],
|
|
271
|
-
duration=user_icon.info["duration"],
|
|
96
|
+
hits_change = f"({op}{value:,})" if value != 0 else None
|
|
97
|
+
badges = [Badge(**i.model_dump()) for i in info.badges]
|
|
98
|
+
draw_user = DrawUser(
|
|
99
|
+
id=info.id,
|
|
100
|
+
username=info.username,
|
|
101
|
+
country_code=info.country_code,
|
|
102
|
+
mode=mode.upper(),
|
|
103
|
+
badges=badges,
|
|
104
|
+
team=info.team.model_dump() if info.team else None,
|
|
105
|
+
statistics=info.statistics.model_dump() if info.statistics else None,
|
|
106
|
+
footer=footer,
|
|
107
|
+
rank_change=rank_change,
|
|
108
|
+
country_rank_change=country_rank_change,
|
|
109
|
+
pp_change=pp_change,
|
|
110
|
+
acc_change=acc_change,
|
|
111
|
+
pc_change=pc_change,
|
|
112
|
+
hits_change=hits_change,
|
|
272
113
|
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
114
|
+
template_path = str(Path(__file__).parent / "info_templates")
|
|
115
|
+
template_name = "index.html"
|
|
116
|
+
template_env = jinja2.Environment( # noqa: S701
|
|
117
|
+
loader=jinja2.FileSystemLoader(template_path),
|
|
118
|
+
enable_async=True,
|
|
119
|
+
)
|
|
120
|
+
template = template_env.get_template(template_name)
|
|
121
|
+
async with get_new_page(2) as page:
|
|
122
|
+
await page.goto(f"file://{template_path}")
|
|
123
|
+
await page.set_content(
|
|
124
|
+
await template.render_async(user_json=draw_user.model_dump_json(), bg=bg), wait_until="networkidle"
|
|
125
|
+
)
|
|
126
|
+
elem = await page.query_selector("#display")
|
|
127
|
+
assert elem
|
|
128
|
+
return await elem.screenshot(type="jpeg")
|