nonebot-plugin-osubot 6.22.1__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.
Files changed (97) hide show
  1. nonebot_plugin_osubot/api.py +13 -7
  2. nonebot_plugin_osubot/config.py +10 -10
  3. nonebot_plugin_osubot/draw/bmap.py +20 -22
  4. nonebot_plugin_osubot/draw/bp.py +3 -13
  5. nonebot_plugin_osubot/draw/catch_preview.py +2 -16
  6. nonebot_plugin_osubot/draw/echarts.py +8 -1
  7. nonebot_plugin_osubot/draw/info.py +59 -207
  8. nonebot_plugin_osubot/draw/info_templates/index.html +507 -0
  9. nonebot_plugin_osubot/draw/info_templates/output.css +2 -0
  10. nonebot_plugin_osubot/draw/map.py +9 -11
  11. nonebot_plugin_osubot/draw/osu_preview.py +50 -0
  12. nonebot_plugin_osubot/draw/osu_preview_templates/css/style.css +258 -0
  13. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/README.md +109 -0
  14. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js +3 -0
  15. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js.map +1 -0
  16. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js +3 -0
  17. nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js.map +1 -0
  18. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/beatmap.js +211 -0
  19. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/hitobject.js +29 -0
  20. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/point.js +55 -0
  21. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/scroll.js +45 -0
  22. nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/timingpoint.js +35 -0
  23. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/LegacyRandom.js +81 -0
  24. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/PalpableCatchHitObject.js +53 -0
  25. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/bananashower.js +33 -0
  26. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/catch.js +211 -0
  27. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/fruit.js +21 -0
  28. nonebot_plugin_osubot/draw/osu_preview_templates/js/catch/juicestream.js +176 -0
  29. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/hitnote.js +21 -0
  30. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/holdnote.js +37 -0
  31. nonebot_plugin_osubot/draw/osu_preview_templates/js/mania/mania.js +164 -0
  32. nonebot_plugin_osubot/draw/osu_preview_templates/js/preview.js +61 -0
  33. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/bezier2.js +33 -0
  34. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/catmullcurve.js +34 -0
  35. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/centripetalcatmullrom.js +30 -0
  36. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/circumstancedcircle.js +47 -0
  37. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curve.js +25 -0
  38. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/curvetype.js +17 -0
  39. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/equaldistancemulticurve.js +70 -0
  40. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/curve/linearbezier.js +40 -0
  41. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/hitcircle.js +85 -0
  42. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/slider.js +120 -0
  43. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/spinner.js +56 -0
  44. nonebot_plugin_osubot/draw/osu_preview_templates/js/standard/standard.js +170 -0
  45. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/donkat.js +40 -0
  46. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/drumroll.js +34 -0
  47. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/shaker.js +58 -0
  48. nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/taiko.js +120 -0
  49. nonebot_plugin_osubot/draw/osu_preview_templates/js/util.js +61 -0
  50. nonebot_plugin_osubot/draw/osu_preview_templates/pic.html +110 -0
  51. nonebot_plugin_osubot/draw/rating.py +6 -3
  52. nonebot_plugin_osubot/draw/score.py +23 -81
  53. nonebot_plugin_osubot/draw/taiko_preview.py +14 -13
  54. nonebot_plugin_osubot/draw/templates/bpa_chart.html +1 -1
  55. nonebot_plugin_osubot/draw/utils.py +162 -16
  56. nonebot_plugin_osubot/file.py +184 -31
  57. nonebot_plugin_osubot/mania/__init__.py +9 -10
  58. nonebot_plugin_osubot/matcher/__init__.py +2 -0
  59. nonebot_plugin_osubot/matcher/bp_analyze.py +14 -9
  60. nonebot_plugin_osubot/matcher/guess.py +250 -294
  61. nonebot_plugin_osubot/matcher/map_convert.py +21 -13
  62. nonebot_plugin_osubot/matcher/medal.py +1 -1
  63. nonebot_plugin_osubot/matcher/osudl.py +5 -4
  64. nonebot_plugin_osubot/matcher/pr.py +0 -4
  65. nonebot_plugin_osubot/matcher/preview.py +10 -3
  66. nonebot_plugin_osubot/matcher/recommend.py +7 -12
  67. nonebot_plugin_osubot/mods.py +62 -61
  68. nonebot_plugin_osubot/network/first_response.py +1 -1
  69. nonebot_plugin_osubot/osufile/mods/AP.png +0 -0
  70. nonebot_plugin_osubot/osufile/mods/CL.png +0 -0
  71. nonebot_plugin_osubot/osufile/mods/DT.png +0 -0
  72. nonebot_plugin_osubot/osufile/mods/EZ.png +0 -0
  73. nonebot_plugin_osubot/osufile/mods/FI.png +0 -0
  74. nonebot_plugin_osubot/osufile/mods/FL.png +0 -0
  75. nonebot_plugin_osubot/osufile/mods/HD.png +0 -0
  76. nonebot_plugin_osubot/osufile/mods/HR.png +0 -0
  77. nonebot_plugin_osubot/osufile/mods/HT.png +0 -0
  78. nonebot_plugin_osubot/osufile/mods/MR.png +0 -0
  79. nonebot_plugin_osubot/osufile/mods/NC.png +0 -0
  80. nonebot_plugin_osubot/osufile/mods/NF.png +0 -0
  81. nonebot_plugin_osubot/osufile/mods/PF.png +0 -0
  82. nonebot_plugin_osubot/osufile/mods/RX.png +0 -0
  83. nonebot_plugin_osubot/osufile/mods/SD.png +0 -0
  84. nonebot_plugin_osubot/osufile/mods/SO.png +0 -0
  85. nonebot_plugin_osubot/osufile/mods/TD.png +0 -0
  86. nonebot_plugin_osubot/osufile/mods/V2.png +0 -0
  87. nonebot_plugin_osubot/pp.py +7 -0
  88. nonebot_plugin_osubot/schema/__init__.py +0 -2
  89. nonebot_plugin_osubot/schema/beatmapsets.py +42 -0
  90. nonebot_plugin_osubot/schema/draw_info.py +54 -0
  91. nonebot_plugin_osubot/schema/score.py +2 -0
  92. nonebot_plugin_osubot/schema/user.py +1 -0
  93. {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/METADATA +18 -17
  94. {nonebot_plugin_osubot-6.22.1.dist-info → nonebot_plugin_osubot-6.26.4.dist-info}/RECORD +95 -52
  95. nonebot_plugin_osubot-6.26.4.dist-info/WHEEL +4 -0
  96. nonebot_plugin_osubot/schema/sayo_beatmap.py +0 -59
  97. nonebot_plugin_osubot-6.22.1.dist-info/WHEEL +0 -4
@@ -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, SayoBeatmap, RecommendData
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,7 +122,9 @@ 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
  ),
127
+ beatmapset=i.beatmapset,
126
128
  )
127
129
  for i in scores
128
130
  ]
@@ -188,6 +190,7 @@ async def get_user_scores(
188
190
  ended_at=datetime.strptime(i.play_time, "%Y-%m-%dT%H:%M:%S") + timedelta(hours=8),
189
191
  max_combo=i.max_combo,
190
192
  passed=True,
193
+ pp=i.pp,
191
194
  statistics=NewStatistics(
192
195
  miss=i.nmiss,
193
196
  perfect=i.ngeki,
@@ -411,12 +414,13 @@ async def get_random_bg() -> Optional[bytes]:
411
414
  return res.content
412
415
 
413
416
 
414
- async def get_sayo_map_info(sid, t=0) -> SayoBeatmap:
415
- res = await safe_async_get(f"https://api.sayobot.cn/v2/beatmapinfo?K={sid}&T={t}")
416
- return SayoBeatmap(**res.json())
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)
417
421
 
418
422
 
419
- async def get_map_bg(mapid, sid, bg_name) -> BytesIO:
423
+ async def get_map_bg(mapid, sid, bg_name) -> BytesIO | None:
420
424
  res = await get_first_response(
421
425
  [
422
426
  f"https://catboy.best/preview/background/{mapid}",
@@ -424,7 +428,9 @@ async def get_map_bg(mapid, sid, bg_name) -> BytesIO:
424
428
  f"https://dl.sayobot.cn/beatmaps/files/{sid}/{bg_name}",
425
429
  ]
426
430
  )
427
- return BytesIO(res)
431
+ if res:
432
+ return BytesIO(res.content)
433
+ return None
428
434
 
429
435
 
430
436
  async def get_seasonal_bg() -> Optional[dict]:
@@ -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 get_sayo_map_info
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
- sayo_info = await get_sayo_map_info(mapid)
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.bid_data) > 20:
17
+ if len(data.beatmaps) > 20:
23
18
  im_h = 400 + 102 * 20
24
19
  else:
25
- im_h = 400 + 102 * (len(data.bid_data) - 1)
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.approved_date == -1:
35
+ if not data.ranked_date:
41
36
  approved_date = "谱面状态可能非ranked"
42
37
  else:
43
- datearray = datetime.utcfromtimestamp(data.approved_date)
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
- draw.text((25, 175), f"Source: {data.source}", font=Torus_SemiBold_20, anchor="lt")
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.bid_data[0].length)
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.bid_data, key=lambda k: k.star, reverse=False)
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.mode], font=extra_30, anchor="lt")
56
+ draw.text((20, 320 + h_num), IconLs[cmap.mode_int], font=extra_30, anchor="lt")
61
57
  # 星星
62
- stars_bg = stars_diff(cmap.star)
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.CS, cmap.HP, cmap.OD, cmap.AR)):
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.star < 6.5:
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((65, 335 + h_num), f"★{cmap.star:.2f}", font=Torus_SemiBold_20, anchor="lm", fill=color)
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.bid}",
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.maxcombo}",
113
+ f"Max Combo: {cmap.max_combo or 0}",
116
114
  font=Torus_SemiBold_20,
117
115
  anchor="lm",
118
116
  )
@@ -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 cal_legacy_acc, cal_legacy_rank
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
- if is_lazer:
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 ..file import map_path, download_osu
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
- path = map_path / str(beatmapset_id)
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
- {"name": name, "pp_ls": pp_ls, "length_ls": length_ls, "mod_pp_ls": mod_pp_ls, "mapper_pp_ls": mapper_pp_ls},
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 asyncio
2
- from io import BytesIO
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 ImageDraw, ImageSequence, UnidentifiedImageError
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) -> BytesIO:
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
- bg = Image.open(bg_path)
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
- if bg:
86
- bg = Image.open(BytesIO(bg))
87
- if bg:
88
- bg = bg.convert("RGBA")
89
- width, height = bg.size
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
- draw.text(
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
- draw.text((305, 820), f"{op}{int(value)}", font=Torus_Regular_20)
204
- # SS - A
205
- # gc_x = 493
206
- for gc_num, (_, num) in enumerate(statistics.grade_counts):
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
- t_acc = f"{statistics.hit_accuracy:.2f}%({op}{value:.2f}%)" if value != 0 else f"{statistics.hit_accuracy:.2f}%"
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
- t_pc = f"{statistics.play_count:,}({op}{value:,})" if value != 0 else f"{statistics.play_count:,}"
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
- t_count = f"{statistics.total_hits:,}({op}{value:,})" if value != 0 else f"{statistics.total_hits:,}"
224
- draw.text((935, 1175), t_count, font=Torus_Regular_40, anchor="rt")
225
- # 游玩时间
226
- sec = timedelta(seconds=statistics.play_time)
227
- d_time = datetime(1, 1, 1) + sec
228
- t_time = f"{sec.days}d {d_time.hour}h {d_time.minute}m {d_time.second}s"
229
- draw.text((935, 1245), t_time, font=Torus_Regular_40, anchor="rt")
230
- # 底部时间对比
231
- if day != 0 and user:
232
- day_delta = date.today() - user.date
233
- time = day_delta.days
234
- current_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
235
- draw.text((260, 1305), current_time, font=Torus_Regular_25, anchor="la")
236
- text = f"| 数据对比于 {time} 天前"
237
- draw.text((515, 1305), text, font=Torus_Regular_25, anchor="la")
238
- else:
239
- current_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
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
- gif_frames[0].close()
275
- user_icon.close()
276
- return gif_bytes
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")