nonebot-plugin-osubot 6.24.1__py3-none-any.whl → 6.25.0__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.

Potentially problematic release.


This version of nonebot-plugin-osubot might be problematic. Click here for more details.

@@ -428,7 +428,7 @@ async def get_map_bg(mapid, sid, bg_name) -> BytesIO:
428
428
  f"https://dl.sayobot.cn/beatmaps/files/{sid}/{bg_name}",
429
429
  ]
430
430
  )
431
- return BytesIO(res)
431
+ return BytesIO(res.content)
432
432
 
433
433
 
434
434
  async def get_seasonal_bg() -> Optional[dict]:
@@ -4,6 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
+ <base href="{{ base_url }}">
7
8
  <meta name="robots" content="nofollow, noindex, noarchive">
8
9
  <title>gif生成测试 网址后加#bid</title>
9
10
  </head>
@@ -53,14 +54,14 @@
53
54
  <script src="gif.js/gif.js"></script>
54
55
 
55
56
  <script>
56
- const scaleValue = 0.2;
57
+ const scaleValue = 0.5;
57
58
  const durationValue = 10000;
58
59
  const qualityValue = 10;
59
60
  const timeSpanValue = 50;
60
61
 
61
62
  const createImg = function () {
62
63
  let preview = new Preview(scaleValue);
63
- const osufile = {{ osu_file|tojson }};
64
+ const osufile = `{{ osu_file }}`;
64
65
  preview.load(osufile, function () {
65
66
  let actualStartTime;
66
67
  const previewEndTime = preview.previewTime + durationValue;
@@ -74,7 +75,7 @@
74
75
  const actualEndTime = durationValue === -1 ? preview.endTime : Math.min(actualStartTime + durationValue, preview.endTime);
75
76
  let gif = new GIF({
76
77
  workers: 4,
77
- workerScript: './gif.js/gif.worker.js',
78
+ workerScript: "{{ worker_data_uri }}",
78
79
  quality: qualityValue,
79
80
  width: preview.screen.width * scaleValue,
80
81
  height: preview.screen.height * scaleValue,
@@ -96,18 +97,12 @@
96
97
  currentFrame++;
97
98
  }
98
99
  gif.on('finished', function (blob) {
99
- const url = URL.createObjectURL(blob);
100
- const img = document.createElement('img');
101
- img.id = 'img';
102
- img.src = url;
103
- img.alt = '生成的GIF预览';
100
+ document.getElementById("img").src = URL.createObjectURL(blob);
104
101
  });
105
102
  gif.render();
106
103
  });
107
104
  }
108
-
109
105
  window.addEventListener('hashchange', createImg);
110
-
111
106
  createImg();
112
107
  </script>
113
108
  </body>
@@ -1,10 +1,15 @@
1
+ import base64
2
+ import hashlib
1
3
  import re
4
+ import urllib
2
5
  import random
3
6
  import asyncio
7
+ import uuid
4
8
  from pathlib import Path
5
9
  from typing import Union, Optional
6
10
  from io import BytesIO, TextIOWrapper
7
11
 
12
+ from nonebot.adapters.onebot.v11 import Bot
8
13
  from nonebot.log import logger
9
14
 
10
15
  from .schema import Badge
@@ -18,7 +23,6 @@ user_cache_path = Path() / "data" / "osu" / "user"
18
23
  badge_cache_path = Path() / "data" / "osu" / "badge"
19
24
  team_cache_path = Path() / "data" / "osu" / "team"
20
25
  api_ls = [
21
- "https://api.chimu.moe/v1/download/",
22
26
  "https://osu.direct/api/d/",
23
27
  "https://txy1.sayobot.cn/beatmaps/download/novideo/",
24
28
  "https://catboy.best/d/",
@@ -35,16 +39,55 @@ if not team_cache_path.exists():
35
39
  team_cache_path.mkdir(parents=True, exist_ok=True)
36
40
 
37
41
 
42
+ def extract_filename_from_headers(headers: dict[str, str]) -> Optional[str]:
43
+ """
44
+ 从 Content-Disposition 响应头中提取文件名,并处理 URL 编码。
45
+
46
+ Args:
47
+ headers: 响应头字典。
48
+
49
+ Returns:
50
+ 提取到的文件名字符串,如果失败则返回 None。
51
+ """
52
+ disposition = headers.get("content-disposition", "")
53
+ if not disposition:
54
+ return None
55
+
56
+ match_utf8 = re.search(r"filename\*=(?:utf-8''|)(.+?)(?:;|$)", disposition, re.IGNORECASE)
57
+
58
+ if match_utf8:
59
+ # 提取匹配到的文件名部分
60
+ encoded_filename = match_utf8.group(1).strip('"').strip()
61
+
62
+ try:
63
+ return urllib.parse.unquote(encoded_filename)
64
+ except Exception as e:
65
+ # 如果解码失败,记录错误并尝试使用原始编码
66
+ print(f"警告: 解码 filename* 失败: {e}. 使用原始编码.")
67
+ return encoded_filename
68
+
69
+ match_normal = re.search(r"filename=\"?(.+?)\"?(\s|;|$)", disposition, re.IGNORECASE)
70
+ if match_normal:
71
+ # 普通 filename 字段也可能包含 URL 编码,进行解码
72
+ filename = match_normal.group(1).strip('"').strip()
73
+ try:
74
+ return urllib.parse.unquote(filename)
75
+ except Exception:
76
+ return filename
77
+
78
+ return None
79
+
80
+
38
81
  async def download_map(setid: int) -> Optional[Path]:
39
82
  urls = [i + str(setid) for i in api_ls]
40
83
  logger.info(f"开始下载地图: <{setid}>")
41
84
  req = await get_first_response(urls)
42
- filename = f"{setid}.osz"
85
+ filename = extract_filename_from_headers(req.headers)
43
86
  filepath = map_path.parent / filename
44
- with open(filepath, "wb") as f:
45
- f.write(req.read())
87
+ with open(filepath.absolute(), "wb") as f:
88
+ f.write(req.content)
46
89
  logger.info(f"地图: <{setid}> 下载完毕")
47
- return filepath
90
+ return filepath.absolute()
48
91
 
49
92
 
50
93
  @auto_retry
@@ -61,7 +104,7 @@ async def download_osu(set_id, map_id):
61
104
  filepath = map_path / str(set_id) / filename
62
105
  filepath.parent.mkdir(parents=True, exist_ok=True)
63
106
  with open(filepath, "wb") as f:
64
- f.write(req)
107
+ f.write(req.content)
65
108
  return filepath
66
109
  else:
67
110
  raise Exception("下载出错,请稍后再试")
@@ -125,3 +168,130 @@ async def save_info_pic(user: str, byt: bytes):
125
168
  path.mkdir()
126
169
  with open(path / "info.png", "wb") as f:
127
170
  f.write(BytesIO(byt).getvalue())
171
+
172
+
173
+ def calculate_file_chunks(file_path: str, chunk_size: int = 1024 * 64) -> tuple[list[bytes], str, int]:
174
+ """
175
+ 计算文件分片和 SHA256
176
+
177
+ Args:
178
+ file_path: 文件路径
179
+ chunk_size: 分片大小(默认64KB)
180
+
181
+ Returns:
182
+ (chunks, sha256_hash, total_size)
183
+ """
184
+ chunks = []
185
+ hasher = hashlib.sha256()
186
+ total_size = 0
187
+
188
+ with open(file_path, "rb") as f:
189
+ while True:
190
+ chunk = f.read(chunk_size)
191
+ if not chunk:
192
+ break
193
+ chunks.append(chunk)
194
+ hasher.update(chunk)
195
+ total_size += len(chunk)
196
+
197
+ sha256_hash = hasher.hexdigest()
198
+
199
+ return chunks, sha256_hash, total_size
200
+
201
+
202
+ MAX_CONCURRENT_UPLOADS = 20
203
+
204
+
205
+ async def _upload_chunk(
206
+ bot: "Bot",
207
+ stream_id: str,
208
+ chunk_data: bytes,
209
+ chunk_index: int,
210
+ total_chunks: int,
211
+ total_size: int,
212
+ sha256_hash: str,
213
+ filename: str,
214
+ semaphore: asyncio.Semaphore,
215
+ ) -> None:
216
+ """内部函数,用于异步上传单个文件分片"""
217
+ async with semaphore:
218
+ # 将分片数据编码为 base64
219
+ chunk_base64 = base64.b64encode(chunk_data).decode("utf-8")
220
+
221
+ # 构建参数
222
+ params = {
223
+ "stream_id": stream_id,
224
+ "chunk_data": chunk_base64,
225
+ "chunk_index": chunk_index,
226
+ "total_chunks": total_chunks,
227
+ "file_size": total_size,
228
+ "expected_sha256": sha256_hash,
229
+ "filename": filename,
230
+ "file_retention": 30 * 1000,
231
+ }
232
+
233
+ # 发送分片
234
+ response = await bot.call_api("upload_file_stream", **params)
235
+
236
+ logger.info(
237
+ f"分片 {chunk_index + 1}/{total_chunks} 上传成功 "
238
+ f"(接收: {response.get('received_chunks', 0)}/{response.get('total_chunks', 0)})"
239
+ )
240
+
241
+
242
+ async def upload_file_stream_batch(bot: Bot, file_path: Path, chunk_size: int = 1024 * 64) -> str:
243
+ """
244
+ 一次性批量上传文件流
245
+
246
+ Args:
247
+ bot: Bot 实例
248
+ file_path: 要上传的文件路径
249
+ chunk_size: 分片大小
250
+
251
+ Returns:
252
+ 上传完成后的文件路径
253
+ """
254
+ if not file_path.exists():
255
+ raise FileNotFoundError(f"文件不存在: {file_path}")
256
+
257
+ # 分析文件
258
+ chunks, sha256_hash, total_size = calculate_file_chunks(str(file_path), chunk_size)
259
+ stream_id = str(uuid.uuid4())
260
+
261
+ logger.info(f"\n开始上传文件: {file_path.name}")
262
+ logger.info(f"流ID: {stream_id}")
263
+
264
+ # 一次性发送所有分片
265
+ total_chunks = len(chunks)
266
+ # 创建信号量,限制最大并发数
267
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_UPLOADS)
268
+
269
+ # 创建所有分片上传任务
270
+ upload_tasks = []
271
+ for chunk_index, chunk_data in enumerate(chunks):
272
+ task = _upload_chunk(
273
+ bot, stream_id, chunk_data, chunk_index, total_chunks, total_size, sha256_hash, file_path.name, semaphore
274
+ )
275
+ upload_tasks.append(task)
276
+
277
+ try:
278
+ await asyncio.gather(*upload_tasks)
279
+ except Exception as e:
280
+ logger.error(f"\n文件分片上传过程中发生错误: {e}")
281
+ # 这里可以选择执行清理逻辑,如通知服务器取消上传
282
+ raise e
283
+
284
+ # 发送完成信号
285
+ logger.info("\n所有分片发送完成,请求文件合并...")
286
+ complete_params = {"stream_id": stream_id, "is_complete": True}
287
+
288
+ response = await bot.call_api("upload_file_stream", **complete_params)
289
+
290
+ if response.get("status") == "file_complete":
291
+ logger.info("✅ 文件上传成功!")
292
+ logger.info(f" - 文件路径: {response.get('file_path')}")
293
+ logger.info(f" - 文件大小: {response.get('file_size')} 字节")
294
+ logger.info(f" - SHA256: {response.get('sha256')}")
295
+ return response.get("file_path")
296
+ else:
297
+ raise Exception(f"文件状态异常: {response}")
@@ -5,6 +5,7 @@ from .getbg import getbg
5
5
  from .match import match
6
6
  from .medal import medal
7
7
  from .score import score
8
+ from .osudl import osudl
8
9
  from .pr import pr, recent
9
10
  from .rating import rating
10
11
  from .history import history
@@ -57,4 +58,5 @@ __all__ = [
57
58
  "match",
58
59
  "rating",
59
60
  "group_pp_rank",
61
+ "osudl",
60
62
  ]
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
3
4
  from nonebot.rule import ArgumentParser
4
5
  from nonebot.internal.adapter import Message
5
6
  from nonebot_plugin_alconna import UniMessage
@@ -10,6 +11,7 @@ from nonebot.params import CommandArg, ShellCommandArgv
10
11
  from ..api import get_beatmapsets_info, osu_api
11
12
  from ..mania import Options, convert_mania_map
12
13
  from ..schema import Beatmap
14
+ from ..file import upload_file_stream_batch
13
15
 
14
16
  parser = ArgumentParser("convert", description="变换mania谱面")
15
17
  parser.add_argument("--set", type=int, help="要转换的谱面的setid")
@@ -38,7 +40,7 @@ async def _(argv: list[str] = ShellCommandArgv()):
38
40
  return
39
41
  options = Options(**vars(args))
40
42
  if options.map:
41
- map_data = await osu_api("map", options.map)
43
+ map_data = await osu_api("map", map_id=options.map)
42
44
  mapinfo = Beatmap(**map_data)
43
45
  beatmapsets_info = await get_beatmapsets_info(mapinfo.beatmapset_id)
44
46
  options.set = mapinfo.beatmapset_id
@@ -67,7 +69,7 @@ change = on_command("倍速", priority=11, block=True)
67
69
 
68
70
 
69
71
  @change.handle()
70
- async def _(msg: Message = CommandArg()):
72
+ async def _(bot: Bot, event: GroupMessageEvent, msg: Message = CommandArg()):
71
73
  args = msg.extract_plain_text().strip().split()
72
74
  argv = ["--map"]
73
75
  if not args:
@@ -88,7 +90,7 @@ async def _(msg: Message = CommandArg()):
88
90
  args = parser.parse_args(argv)
89
91
  options = Options(**vars(args))
90
92
  if options.map:
91
- map_data = await osu_api("map", options.map)
93
+ map_data = await osu_api("map", map_id=options.map)
92
94
  mapinfo = Beatmap(**map_data)
93
95
  beatmapsets_info = await get_beatmapsets_info(mapinfo.beatmapset_id)
94
96
  options.set = mapinfo.beatmapset_id
@@ -97,9 +99,10 @@ async def _(msg: Message = CommandArg()):
97
99
  if not osz_path:
98
100
  await UniMessage.text("未找到该地图,请检查是否搞混了mapID与setID").finish(reply_to=True)
99
101
  file_path = osz_path.absolute()
102
+ server_osz_path = await upload_file_stream_batch(bot, file_path)
103
+
100
104
  try:
101
- with open(file_path, "rb") as f:
102
- await UniMessage.file(raw=f.read()).send()
105
+ await bot.call_api("upload_group_file", group_id=event.group_id, file=server_osz_path, name=osz_path.name)
103
106
  except ActionFailed:
104
107
  await UniMessage.text("上传文件失败,可能是群空间满或没有权限导致的").send(reply_to=True)
105
108
  finally:
@@ -113,7 +116,7 @@ generate_full_ln = on_command("反键", priority=11, block=True)
113
116
 
114
117
 
115
118
  @generate_full_ln.handle()
116
- async def _(msg: Message = CommandArg()):
119
+ async def _(bot: Bot, event: GroupMessageEvent, msg: Message = CommandArg()):
117
120
  args = msg.extract_plain_text().strip().split()
118
121
  if not args:
119
122
  await UniMessage.text("请输入需要转ln的地图setID").finish(reply_to=True)
@@ -133,9 +136,9 @@ async def _(msg: Message = CommandArg()):
133
136
  if not osz_path:
134
137
  await UniMessage.text("未找到该地图,请检查是否搞混了mapID与setID").finish(reply_to=True)
135
138
  file_path = osz_path.absolute()
139
+ server_osz_path = await upload_file_stream_batch(bot, file_path)
136
140
  try:
137
- with open(file_path, "rb") as f:
138
- await UniMessage.file(raw=f.read()).send()
141
+ await bot.call_api("upload_group_file", group_id=event.group_id, file=server_osz_path, name=osz_path.name)
139
142
  except ActionFailed:
140
143
  await UniMessage.text("上传文件失败,可能是群空间满或没有权限导致的").send(reply_to=True)
141
144
  finally:
@@ -1,22 +1,23 @@
1
1
  from nonebot import on_command
2
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
2
3
  from nonebot.params import CommandArg
3
4
  from nonebot.internal.adapter import Message
4
5
  from nonebot_plugin_alconna import UniMessage
5
6
 
6
- from ..file import download_map
7
+ from ..file import download_map, upload_file_stream_batch
7
8
 
8
9
  osudl = on_command("osudl", priority=11, block=True)
9
10
 
10
11
 
11
12
  @osudl.handle()
12
- async def _osudl(setid: Message = CommandArg()):
13
+ async def _osudl(bot: Bot, event: GroupMessageEvent, setid: Message = CommandArg()):
13
14
  setid = setid.extract_plain_text().strip()
14
15
  if not setid or not setid.isdigit():
15
16
  await UniMessage.text("请输入正确的地图ID").send(reply_to=True)
16
17
  osz_path = await download_map(int(setid))
17
- file_path = osz_path.absolute()
18
+ server_osz_path = await upload_file_stream_batch(bot, osz_path)
18
19
  try:
19
- await UniMessage.file(path=file_path).send()
20
+ await bot.call_api("upload_group_file", group_id=event.group_id, file=server_osz_path, name=osz_path.name)
20
21
  except Exception:
21
22
  await UniMessage.text("上传文件失败,可能是群空间满或没有权限导致的").send(reply_to=True)
22
23
  finally:
@@ -43,7 +43,7 @@ async def handle_recommend(state: T_State, matcher: type[Matcher]):
43
43
  await matcher.finish("今天已经没有可以推荐的图啦,明天再来吧")
44
44
  return None
45
45
  bid = int(re.findall("https://osu.ppy.sh/beatmaps/(.*)", recommend_map.mapLink)[0])
46
- map_data = await osu_api("map", bid)
46
+ map_data = await osu_api("map", map_id=bid)
47
47
  map_info = Beatmap(**map_data)
48
48
  sid = map_info.beatmapset_id
49
49
  s = (
@@ -25,6 +25,6 @@ async def get_first_response(urls: list[str]):
25
25
  for task in done:
26
26
  response = task.result()
27
27
  if response is not None and response.status_code == 200:
28
- return response.content
28
+ return response
29
29
  tasks = [task for task in tasks if not task.done()]
30
30
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-osubot
3
- Version: 6.24.1
3
+ Version: 6.25.0
4
4
  Summary: OSUbot in NoneBot2
5
5
  License: AGPL-3.0
6
6
  Author-email: yaowan233 <572473053@qq.com>
@@ -8,6 +8,7 @@ Requires-Python: >=3.9.8,<3.13
8
8
  Requires-Dist: expiringdict>=1.2.2
9
9
  Requires-Dist: httpx>=0.23.3
10
10
  Requires-Dist: matplotlib>=3.7.1
11
+ Requires-Dist: nonebot-adapter-onebot>=2.4.6
11
12
  Requires-Dist: nonebot-plugin-alconna>=0.46.4
12
13
  Requires-Dist: nonebot-plugin-apscheduler>=0.4.0
13
14
  Requires-Dist: nonebot-plugin-htmlrender>=0.3.1
@@ -1,5 +1,5 @@
1
1
  nonebot_plugin_osubot/__init__.py,sha256=Q-mTTnOIdKiKG6JrVm-kqpPrAhOP9lWyiKHNRqA7gpc,1478
2
- nonebot_plugin_osubot/api.py,sha256=3jJbgJdL1sJ_FcbTqJQxRPLlJueaUXhH0REfy8Ff-ps,16521
2
+ nonebot_plugin_osubot/api.py,sha256=ZPbtDWz_zBUaL-3RnpXJuDhZ-RAVPCzvHp4MHShpGiE,16529
3
3
  nonebot_plugin_osubot/beatmap_stats_moder.py,sha256=mNNTufc-gvO4NdYa3TnealSZI4-LBoiTlb599SeLBck,2915
4
4
  nonebot_plugin_osubot/config.py,sha256=Ub2s5Ny09-d1ZwT6x8cirB6zWy0brtO-oZV3W0qEM5Q,311
5
5
  nonebot_plugin_osubot/data/osu/1849145.osz,sha256=enbHOvDu6ZkvQBM7gtvgZBY-r0a7z87pG62Xm9hXUSI,6933013
@@ -50,7 +50,6 @@ nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js,sha256=qLERBxuzsS
50
50
  nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.js.map,sha256=1Hg6tubanG4YHx_ezCr88Kkcf-po-RxFm2ovxXt8h-Q,29120
51
51
  nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js,sha256=yp4wSFV-wF1hnhi4NAPNNmnIiTnl-i1gNM52JdRFlw0,16636
52
52
  nonebot_plugin_osubot/draw/osu_preview_templates/gif.js/gif.worker.js.map,sha256=gasS_2E9Lp3sbSC0PtRd70pW7BzYqw7lMcC65aD2k2M,55277
53
- nonebot_plugin_osubot/draw/osu_preview_templates/index.html,sha256=x4QQ0c1vMIgGkxdapplruSr1Z97bULE9CBY3pkjTvS8,16804
54
53
  nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/beatmap.js,sha256=xra2zaBmCtlw-_4uhZM_ipvk_CaakOYIeygREIHDmEM,5338
55
54
  nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/hitobject.js,sha256=q9b2KA2qHkWkkGzEq1Djk5RwVr538E0B22jNIIU9n84,736
56
55
  nonebot_plugin_osubot/draw/osu_preview_templates/js/beatmap/point.js,sha256=AXybWPbpUypXCVdz_Vqdk8HjEgDQd6cT-360gGmrCas,1035
@@ -83,7 +82,7 @@ nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/drumroll.js,sha256=3Fj
83
82
  nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/shaker.js,sha256=oueLYtlQCDBpBv_JNwJz_RY6PbDCBbd837NeF8o82T8,1900
84
83
  nonebot_plugin_osubot/draw/osu_preview_templates/js/taiko/taiko.js,sha256=TEOiZydvVlGabGebv2V6kSfikc5-S2oUmbwhFfvDxPk,3296
85
84
  nonebot_plugin_osubot/draw/osu_preview_templates/js/util.js,sha256=XveUlX-d0vrUnXaGbR8y428s6Nw2zhDR235pFko_MxM,1504
86
- nonebot_plugin_osubot/draw/osu_preview_templates/pic.html,sha256=Vlc5TrM-NDb2v0bgVQo63dDW89AbOP9k--7CkiRTiFo,4421
85
+ nonebot_plugin_osubot/draw/osu_preview_templates/pic.html,sha256=q8FgImgnH-FOPRGMtecD6DwN1B-iiLX1SLM-vMQFXy8,4285
87
86
  nonebot_plugin_osubot/draw/rating.py,sha256=pA7mGLI4IujmYB6kQf_tSkR7mZGpUAVLRLyaAsZhqTM,20397
88
87
  nonebot_plugin_osubot/draw/score.py,sha256=cUD8zpdf8e43Du9Z_A6T3HIP3IxtgSF3wheaNwMBWWk,29277
89
88
  nonebot_plugin_osubot/draw/static.py,sha256=wdlzNO3xyiauKiMLr_h-B9uAsFU7fX_Y-fOusYKZP3k,4132
@@ -94,12 +93,12 @@ nonebot_plugin_osubot/draw/templates/mod_chart.html,sha256=Iz71KM5v9z_Rt2vqJ5WIZ
94
93
  nonebot_plugin_osubot/draw/templates/pp_rank_line_chart.html,sha256=Gyf-GR8ZBlWQTks0TlB3M8EWUBMVwiUaesFAmDISxLo,1417
95
94
  nonebot_plugin_osubot/draw/utils.py,sha256=6QDbByPQZCxI0k_i5MsExyWZ-sHgJUw6nEWLv85IgLY,15826
96
95
  nonebot_plugin_osubot/exceptions.py,sha256=N_FsEk-9Eu2QnuznhdfWn6OoyA1HH73Q7bUaY89gVi0,40
97
- nonebot_plugin_osubot/file.py,sha256=kfnaf68q0Au54YMUqhD2nYnusEsHqjxJKy2LqPA8urI,4104
96
+ nonebot_plugin_osubot/file.py,sha256=p8E9oeopvMLT-T_b0PARZifOX_m1HXsxJGGtbZFMVfY,9387
98
97
  nonebot_plugin_osubot/info/__init__.py,sha256=I7YlMQiuHExEeJWqyzZb2I-Vl2uql3Py2LdhSH2Z9N0,136
99
98
  nonebot_plugin_osubot/info/bg.py,sha256=Icua4bS38k0c-WbLUjhfS4IXOF83bgyu_oa2HwX4ZEQ,1541
100
99
  nonebot_plugin_osubot/info/bind.py,sha256=b2ua625hbYym7rpb-kLBB-VDP5YWFdmT5RweM58ggWw,4934
101
100
  nonebot_plugin_osubot/mania/__init__.py,sha256=t5-24nd2FiZTKvMFvNg8ZV9Lp_OFSHjhj_gWUV3s1es,5560
102
- nonebot_plugin_osubot/matcher/__init__.py,sha256=yID7QcdQF6_Mouwbej3JwYUBbKSU3VQdrjq6B1Fz9P8,1331
101
+ nonebot_plugin_osubot/matcher/__init__.py,sha256=0f2_aeiBos3evT9eZRRh73Z0gpdwRcH8XpzUP6jbq-0,1369
103
102
  nonebot_plugin_osubot/matcher/bind.py,sha256=QQJc2S7XFo5tu4CPloIET6fKaeiQixgb8M7QvULV6E0,2834
104
103
  nonebot_plugin_osubot/matcher/bp.py,sha256=GidJfuZ9lJ7LwMq126DDOwMksNUOz4Bkab83OlKg8t8,3983
105
104
  nonebot_plugin_osubot/matcher/bp_analyze.py,sha256=xi40HVOcTvmHWR4WNLm706126CulfpV5UP0500FNiD8,4159
@@ -108,17 +107,17 @@ nonebot_plugin_osubot/matcher/guess.py,sha256=Bv4Rt11eB65hdsPu6KhCjmEP1AacFUwA0u
108
107
  nonebot_plugin_osubot/matcher/history.py,sha256=ZYkVJHdXuVJmhovRhwxFdqNp0o2uJJOACAZhhutyS3w,1577
109
108
  nonebot_plugin_osubot/matcher/info.py,sha256=8CJHTOMTx_nzJ4ZwXo6ZfBwCuO3DtLprRX7jnMtPilk,858
110
109
  nonebot_plugin_osubot/matcher/map.py,sha256=sFpOoFv63y-NOkCJhE6aW0DRYDl_8SoQOPsdq50QxT0,1404
111
- nonebot_plugin_osubot/matcher/map_convert.py,sha256=Q3oNK8NvOUE56mOXQ0PvRQkZZ0nLbtFrAp9wxqw-7Ak,5973
110
+ nonebot_plugin_osubot/matcher/map_convert.py,sha256=oklwbbcrEuous-mtHgGN3bN3PkDqKb95XXjIMSEp5Yk,6343
112
111
  nonebot_plugin_osubot/matcher/match.py,sha256=uyrm8I_WgHK2ya1q46AUxNk_cQiKKh7GKlUzrrG1o7w,472
113
112
  nonebot_plugin_osubot/matcher/medal.py,sha256=LZf8hlXGHy8mdK2l97SsYCChfYYovEDBGNbUPO3AOsw,2967
114
113
  nonebot_plugin_osubot/matcher/mu.py,sha256=l3Ebz47o46EvN2nvo9-zzQI4CTaLMcd5XW0qljqSGIM,445
115
114
  nonebot_plugin_osubot/matcher/osu_help.py,sha256=64rOkYEOETvU8AF__0xLZjVRs3cTac0D1XEultPK_kM,728
116
- nonebot_plugin_osubot/matcher/osudl.py,sha256=yLEblYnLprTf2T00FiRWJ8CuCd0IHyUY9Ka68yAKOXo,872
115
+ nonebot_plugin_osubot/matcher/osudl.py,sha256=aItoFVYgozZHINpBuWv38syixtTOtaTtyBpWKZo94uI,1091
117
116
  nonebot_plugin_osubot/matcher/pr.py,sha256=xGjQvEJHmIZkq9luu8TtbjBB8FykGI4Wzi_-eXghOjQ,4951
118
117
  nonebot_plugin_osubot/matcher/preview.py,sha256=22zNjRdpwxbmIsyZQlUE-qXQBCQCfP_2WobGP7nZZh4,2314
119
118
  nonebot_plugin_osubot/matcher/rank.py,sha256=sFEim3cR_vswzLmbagjqy-ypolcprAxQpawiSEcFqEI,3619
120
119
  nonebot_plugin_osubot/matcher/rating.py,sha256=JY1Q1ELU3Y1FhQ7kVWVkgVsYEVxkAcxjsoMcwC_u234,450
121
- nonebot_plugin_osubot/matcher/recommend.py,sha256=DChL83UNSi_XDHUm1ksPwUgQE12PI_EHaxXkJQoZ5Oc,2521
120
+ nonebot_plugin_osubot/matcher/recommend.py,sha256=4R8rzxi-tC7aCb__64KzAKZo_-ginSb_U0HWK6xaRmI,2528
122
121
  nonebot_plugin_osubot/matcher/score.py,sha256=Nk6dpDlszKJKdboTSQRBf-wMGioHIPqKSVWrnT0Xbns,1212
123
122
  nonebot_plugin_osubot/matcher/update.py,sha256=MHpxoJmU0hKW82XuM9YpyCxUUjjiNKwejnRgYwueR4Q,3168
124
123
  nonebot_plugin_osubot/matcher/update_mode.py,sha256=0Wy6Y1-rN7XcqBZyo37mYFdixq-4HxCwZftUaiYhZqE,1602
@@ -127,7 +126,7 @@ nonebot_plugin_osubot/matcher/utils.py,sha256=gWmNa31wUxcY_PNSNLy348x5_7sTY9ttMK
127
126
  nonebot_plugin_osubot/mods.py,sha256=vxIWYX0HwTxetPAHWZK5ojEMfqV9HFlWT0YC4Yncgb8,1402
128
127
  nonebot_plugin_osubot/network/__init__.py,sha256=WOijcd81yhnpGKYeiDIOxbBDVI12GHPRGoOFfxrUuQk,61
129
128
  nonebot_plugin_osubot/network/auto_retry.py,sha256=vDfYGbEVPF6WZLYXmRVkNvaxf6_6RyIqEAcA7TRwV_k,565
130
- nonebot_plugin_osubot/network/first_response.py,sha256=zETRc6g0AG8ExLyHZTLUl7uzUCdUVIL0IfxvdEtCPt0,932
129
+ nonebot_plugin_osubot/network/first_response.py,sha256=jIYIF476aIUgpIcN08Wo8tXiwu0paNebCcaTuRPmlS4,924
131
130
  nonebot_plugin_osubot/network/manager.py,sha256=x0GI1cFv3m3ZIS4oNJed197PaRo8_Vut_2J7m9ySV30,858
132
131
  nonebot_plugin_osubot/osufile/Best Performance.png,sha256=qBNeZcym5vIqyE23K62ohjVBEPCjlNP9wQgXaT20XyY,704
133
132
  nonebot_plugin_osubot/osufile/History Score.jpg,sha256=yv3-GaJ7sBAbAPMFlUeoyg1PzMhv31Ip5bC4H0qJfSA,836
@@ -448,6 +447,6 @@ nonebot_plugin_osubot/schema/ppysb/__init__.py,sha256=JK2Z4n44gUJPVKdETMJYJ5uIw-
448
447
  nonebot_plugin_osubot/schema/score.py,sha256=o32jKDESzFwOFPZnzjKqxNIj0MPUL9mFvBqgaZARHac,3269
449
448
  nonebot_plugin_osubot/schema/user.py,sha256=sxNmqymG_kIVuGuzfchSv9UML6NPG70cqo2_h5xDIpM,2250
450
449
  nonebot_plugin_osubot/utils/__init__.py,sha256=pyv8XxBcCOeQVDj1E4dgvktzcefgQXfKBlarsYGx1sg,815
451
- nonebot_plugin_osubot-6.24.1.dist-info/WHEEL,sha256=B19PGBCYhWaz2p_UjAoRVh767nYQfk14Sn4TpIZ-nfU,87
452
- nonebot_plugin_osubot-6.24.1.dist-info/METADATA,sha256=AgWa-nFDmhGUDBoZDsF5i7R4BJw1QJ7QxSHReQ3wgAM,4476
453
- nonebot_plugin_osubot-6.24.1.dist-info/RECORD,,
450
+ nonebot_plugin_osubot-6.25.0.dist-info/WHEEL,sha256=B19PGBCYhWaz2p_UjAoRVh767nYQfk14Sn4TpIZ-nfU,87
451
+ nonebot_plugin_osubot-6.25.0.dist-info/METADATA,sha256=Sy7rPJeo1uzQ5ek4gPoImi2JtSt7Nvoe8y-eMIoQ6eQ,4521
452
+ nonebot_plugin_osubot-6.25.0.dist-info/RECORD,,
@@ -1,437 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
- <meta name="robots" content="nofollow, noindex, noarchive">
8
- <title>osu! GIF 预览生成器</title>
9
- <style>
10
- body {
11
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
13
- color: #fff;
14
- margin: 0;
15
- padding: 20px;
16
- min-height: 100vh;
17
- }
18
-
19
- .container {
20
- max-width: 1200px;
21
- margin: 0 auto;
22
- display: flex;
23
- flex-direction: column;
24
- align-items: center;
25
- }
26
-
27
- .header {
28
- text-align: center;
29
- margin-bottom: 30px;
30
- width: 100%;
31
- }
32
-
33
- h1 {
34
- color: #ff66aa;
35
- text-shadow: 0 0 10px rgba(255, 102, 170, 0.5);
36
- margin-bottom: 10px;
37
- }
38
-
39
- .instructions {
40
- background: rgba(0, 0, 0, 0.3);
41
- padding: 15px;
42
- border-radius: 10px;
43
- margin-bottom: 20px;
44
- width: 100%;
45
- box-sizing: border-box;
46
- }
47
-
48
- .input-area {
49
- display: flex;
50
- flex-wrap: wrap;
51
- gap: 10px;
52
- margin-bottom: 20px;
53
- width: 100%;
54
- justify-content: center;
55
- }
56
-
57
- .input-group {
58
- display: flex;
59
- align-items: center;
60
- gap: 5px;
61
- }
62
-
63
- .input-group label {
64
- white-space: nowrap;
65
- }
66
-
67
- input[type="text"] {
68
- padding: 12px;
69
- border: none;
70
- border-radius: 5px;
71
- background: rgba(255, 255, 255, 0.1);
72
- color: white;
73
- width: 200px;
74
- }
75
-
76
- input[type="checkbox"] {
77
- width: 16px;
78
- height: 16px;
79
- }
80
-
81
- .checkbox-group {
82
- display: flex;
83
- align-items: center;
84
- gap: 5px;
85
- margin-left: 10px;
86
- }
87
-
88
- button {
89
- padding: 12px 20px;
90
- border: none;
91
- border-radius: 5px;
92
- background: #ff66aa;
93
- color: white;
94
- cursor: pointer;
95
- transition: background 0.3s;
96
- }
97
-
98
- button:hover {
99
- background: #ff3388;
100
- }
101
-
102
- .status {
103
- margin: 20px 0;
104
- padding: 15px;
105
- border-radius: 5px;
106
- background: rgba(0, 0, 0, 0.3);
107
- width: 100%;
108
- text-align: center;
109
- box-sizing: border-box;
110
- }
111
-
112
- .progress-container {
113
- width: 100%;
114
- background: rgba(255, 255, 255, 0.1);
115
- border-radius: 5px;
116
- margin: 20px 0;
117
- overflow: hidden;
118
- display: none;
119
- }
120
-
121
- .progress-bar {
122
- width: 0%;
123
- height: 20px;
124
- background: linear-gradient(90deg, #ff66aa 0%, #ff3388 100%);
125
- transition: width 0.3s;
126
- }
127
-
128
- #result {
129
- margin-top: 30px;
130
- text-align: center;
131
- width: 100%;
132
- }
133
-
134
- #img {
135
- max-width: 100%;
136
- border-radius: 10px;
137
- box-shadow: 0 0 20px rgba(255, 102, 170, 0.5);
138
- }
139
-
140
- .footer {
141
- margin-top: 30px;
142
- text-align: center;
143
- font-size: 0.9em;
144
- color: #aaa;
145
- }
146
- </style>
147
- </head>
148
-
149
- <body>
150
- <div class="container">
151
- <div class="header">
152
- <h1>osu! GIF 预览生成器</h1>
153
- <div class="instructions">
154
- <p>输入osu!谱面ID,生成游戏预览GIF</p>
155
- </div>
156
- </div>
157
-
158
- <div class="input-area">
159
- <div class="input-group">
160
- <label>谱面ID:</label>
161
- <input type="text" value="172662" id="beatmapInput" placeholder="例如: 172662" />
162
- </div>
163
-
164
- <div class="input-group">
165
- <label>缩放比例:</label>
166
- <input type="text" value="0.2" id="scaleInput" placeholder="建议0.2,0.5以上生成速度显著降低" />
167
- </div>
168
-
169
- <div class="input-group">
170
- <label>帧间隔(ms):</label>
171
- <input type="text" value="50" id="timeSpanInput" placeholder="建议50,最低15" />
172
- </div>
173
-
174
- <div class="input-group">
175
- <label>开始时间(ms):</label>
176
- <input type="text" value="-1" id="startTimeInput" placeholder="-1表示第一个note前" />
177
- <div class="checkbox-group">
178
- <input type="checkbox" id="usePreviewTimeCheckbox" checked />
179
- <label for="usePreviewTimeCheckbox">优先使用谱面预览时间</label>
180
- </div>
181
- </div>
182
-
183
- <div class="input-group">
184
- <label>持续时间(ms):</label>
185
- <input type="text" value="-1" id="durationInput" placeholder="-1表示到结尾" />
186
- </div>
187
-
188
- <div class="input-group">
189
- <label>GIF质量:</label>
190
- <input type="text" value="10" id="qualityInput" placeholder="不建议修改,值越小质量越好,但是经测试区别非常小" />
191
- </div>
192
-
193
- <button id="generateBtn">生成GIF</button>
194
- </div>
195
-
196
- <div class="status" id="status">准备就绪,请输入谱面ID</div>
197
-
198
- <div class="progress-container" id="progressContainer">
199
- <div class="progress-bar" id="progressBar"></div>
200
- </div>
201
-
202
- <div id="result"></div>
203
-
204
- <div class="footer">
205
- <p>使用 osu.direct API 获取谱面数据 | 基于 gif.js 生成GIF</p>
206
- </div>
207
- </div>
208
-
209
- <script src="js/util.js"></script>
210
-
211
- <script src="js/beatmap/beatmap.js"></script>
212
- <script src="js/beatmap/timingpoint.js"></script>
213
- <script src="js/beatmap/hitobject.js"></script>
214
- <script src="js/beatmap/point.js"></script>
215
- <script src="js/beatmap/scroll.js"></script>
216
-
217
- <script src="js/standard/standard.js"></script>
218
- <script src="js/standard/hitcircle.js"></script>
219
- <script src="js/standard/slider.js"></script>
220
- <script src="js/standard/curve/curve.js"></script>
221
- <script src="js/standard/curve/equaldistancemulticurve.js"></script>
222
- <script src="js/standard/curve/linearbezier.js"></script>
223
- <script src="js/standard/curve/catmullcurve.js"></script>
224
- <script src="js/standard/curve/curvetype.js"></script>
225
- <script src="js/standard/curve/bezier2.js"></script>
226
- <script src="js/standard/curve/centripetalcatmullrom.js"></script>
227
- <script src="js/standard/curve/circumstancedcircle.js"></script>
228
- <script src="js/standard/spinner.js"></script>
229
-
230
- <script src="js/taiko/taiko.js"></script>
231
- <script src="js/taiko/donkat.js"></script>
232
- <script src="js/taiko/drumroll.js"></script>
233
- <script src="js/taiko/shaker.js"></script>
234
-
235
- <script src="js/catch/LegacyRandom.js"></script>
236
- <script src="js/catch/catch.js"></script>
237
- <script src="js/catch/fruit.js"></script>
238
- <script src="js/catch/bananashower.js"></script>
239
- <script src="js/catch/juicestream.js"></script>
240
- <script src="js/catch/PalpableCatchHitObject.js"></script>
241
-
242
- <script src="js/mania/mania.js"></script>
243
- <script src="js/mania/hitnote.js"></script>
244
- <script src="js/mania/holdnote.js"></script>
245
-
246
- <script src="js/preview.js"></script>
247
- <script src="gif.js/gif.js"></script>
248
-
249
- <script>
250
- document.addEventListener('DOMContentLoaded', function () {
251
- const beatmapInput = document.getElementById('beatmapInput');
252
- const scaleInput = document.getElementById('scaleInput');
253
- const timeSpanInput = document.getElementById('timeSpanInput');
254
- const startTimeInput = document.getElementById('startTimeInput');
255
- const durationInput = document.getElementById('durationInput');
256
- const qualityInput = document.getElementById('qualityInput');
257
- const usePreviewTimeCheckbox = document.getElementById('usePreviewTimeCheckbox');
258
- const generateBtn = document.getElementById('generateBtn');
259
- const status = document.getElementById('status');
260
- const progressContainer = document.getElementById('progressContainer');
261
- const progressBar = document.getElementById('progressBar');
262
- const result = document.getElementById('result');
263
-
264
- generateBtn.addEventListener('click', function () {
265
- const beatmapID = beatmapInput.value.trim();
266
- const scaleValue = parseFloat(scaleInput.value.trim());
267
- const timeSpanValue = parseInt(timeSpanInput.value.trim());
268
- const startTimeValue = parseInt(startTimeInput.value.trim());
269
- const durationValue = parseInt(durationInput.value.trim());
270
- const qualityValue = parseInt(qualityInput.value.trim());
271
- const usePreviewTime = usePreviewTimeCheckbox.checked;
272
-
273
- // 验证输入
274
- if (isNaN(scaleValue) || scaleValue <= 0 || scaleValue > 1) {
275
- status.textContent = '请输入有效的缩放比例 (0 < 比例 ≤ 1)';
276
- status.style.color = '#ff6666';
277
- return;
278
- }
279
- if (!beatmapID) {
280
- status.textContent = '请输入有效的谱面ID';
281
- status.style.color = '#ff6666';
282
- return;
283
- }
284
- if (isNaN(timeSpanValue) || timeSpanValue < 15) {
285
- status.textContent = '请输入有效的帧间隔时间';
286
- status.style.color = '#ff6666';
287
- return;
288
- }
289
- if (isNaN(startTimeValue) || (startTimeValue < 0 && startTimeValue !== -1)) {
290
- status.textContent = '请输入有效的开始时间';
291
- status.style.color = '#ff6666';
292
- return;
293
- }
294
- if (isNaN(durationValue) || (durationValue < 0 && durationValue !== -1)) {
295
- status.textContent = '请输入有效的持续时间';
296
- status.style.color = '#ff6666';
297
- return;
298
- }
299
- if (isNaN(qualityValue) || qualityValue < 1 || qualityValue > 30) {
300
- status.textContent = '请输入有效的GIF质量 (1-30)';
301
- status.style.color = '#ff6666';
302
- return;
303
- }
304
-
305
- generateGIF(beatmapID, scaleValue, timeSpanValue, startTimeValue, durationValue, qualityValue, usePreviewTime);
306
- });
307
-
308
- function generateGIF(beatmapID, scaleValue, timeSpanValue, startTimeValue, durationValue, qualityValue, usePreviewTime) {
309
- status.textContent = '正在获取谱面数据...';
310
- status.style.color = '#fff';
311
- progressContainer.style.display = 'block';
312
- progressBar.style.width = '10%';
313
-
314
- // 清除之前的结果
315
- result.innerHTML = '';
316
-
317
- var osuFileUrl = "https://osu.direct/api/osu/" + beatmapID;
318
- !async function () {
319
- var self = this;
320
- let osufile;
321
- try {
322
- const response = await fetch(osuFileUrl);
323
- osufile = await response.text();
324
- }
325
- catch (e) {
326
- console.log(e);
327
- document.title = "从osu.direct获取谱面文件失败";
328
- }
329
- if (!osufile) {
330
- return;
331
- }
332
-
333
- var preview = new Preview(scaleValue);
334
- preview.load(osufile, function () {
335
- status.textContent = '加载帧...';
336
- progressBar.style.width = '30%';
337
-
338
- // 计算实际开始时间
339
- let actualStartTime;
340
-
341
- if (usePreviewTime && preview.previewTime >= 0) {
342
- // 检查预览时间是否有效
343
- const previewEndTime = durationValue === -1 ? preview.endTime : preview.previewTime + durationValue;
344
-
345
- if (previewEndTime <= preview.endTime) {
346
- // 使用谱面预览时间
347
- actualStartTime = preview.previewTime;
348
- status.textContent += ' (使用谱面预览时间)';
349
- } else {
350
- // 预览时间加上持续时间超过谱面结束时间,使用用户指定的开始时间
351
- actualStartTime = startTimeValue === -1 ? preview.startTime : Math.max(startTimeValue, 0);
352
- }
353
- } else {
354
- // 使用用户指定的开始时间
355
- actualStartTime = startTimeValue === -1 ? preview.startTime : Math.max(startTimeValue, 0);
356
- }
357
-
358
- const actualEndTime = durationValue === -1 ? preview.endTime : Math.min(actualStartTime + durationValue, preview.endTime);
359
-
360
- var gif = new GIF({
361
- workers: 4,
362
- workerScript: './gif.js/gif.worker.js',
363
- quality: qualityValue,
364
- width: preview.screen.width * scaleValue,
365
- height: preview.screen.height * scaleValue,
366
- //transparent: "0x000000",
367
- });
368
-
369
- const totalFrames = Math.ceil((actualEndTime - actualStartTime) / timeSpanValue);
370
- let currentFrame = 0;
371
-
372
- // 添加帧
373
- while (currentFrame <= totalFrames) {
374
- const t = actualStartTime + currentFrame * timeSpanValue;
375
- preview.at(t);
376
-
377
- gif.addFrame(preview.ctx, {
378
- copy: true,
379
- delay: timeSpanValue
380
- });
381
-
382
- currentFrame++;
383
- progressBar.style.width = (30 + (currentFrame / totalFrames) * 60) + '%';
384
- }
385
- // 所有帧已添加
386
- progressBar.style.width = '95%';
387
- status.textContent = '最终处理中...';
388
-
389
- gif.on('finished', function (blob) {
390
- const url = URL.createObjectURL(blob);
391
-
392
- const img = document.createElement('img');
393
- img.id = 'img';
394
- img.src = url;
395
- img.alt = '生成的GIF预览';
396
-
397
- const downloadLink = document.createElement('a');
398
- downloadLink.href = url;
399
- downloadLink.download = `osu-preview-${beatmapID}.gif`;
400
- downloadLink.textContent = '下载GIF';
401
- downloadLink.className = 'download-btn';
402
- downloadLink.style.display = 'inline-block';
403
- downloadLink.style.marginTop = '15px';
404
- downloadLink.style.padding = '10px 15px';
405
- downloadLink.style.backgroundColor = '#ff66aa';
406
- downloadLink.style.color = 'white';
407
- downloadLink.style.borderRadius = '5px';
408
- downloadLink.style.textDecoration = 'none';
409
-
410
- result.appendChild(img);
411
- result.appendChild(document.createElement('br'));
412
- result.appendChild(downloadLink);
413
-
414
- progressBar.style.width = '100%';
415
- status.textContent = 'GIF生成完成!';
416
-
417
- //progressContainer.style.display = 'none';
418
- });
419
-
420
- gif.render();
421
-
422
- }, function (error) {
423
- status.textContent = '错误: ' + error;
424
- status.style.color = '#ff6666';
425
- progressContainer.style.display = 'none';
426
- });
427
-
428
-
429
- }();
430
-
431
-
432
- }
433
- });
434
- </script>
435
- </body>
436
-
437
- </html>