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.
- nonebot_plugin_osubot/api.py +1 -1
- nonebot_plugin_osubot/draw/osu_preview_templates/pic.html +5 -10
- nonebot_plugin_osubot/file.py +176 -6
- nonebot_plugin_osubot/matcher/__init__.py +2 -0
- nonebot_plugin_osubot/matcher/map_convert.py +11 -8
- nonebot_plugin_osubot/matcher/osudl.py +5 -4
- nonebot_plugin_osubot/matcher/recommend.py +1 -1
- nonebot_plugin_osubot/network/first_response.py +1 -1
- {nonebot_plugin_osubot-6.24.1.dist-info → nonebot_plugin_osubot-6.25.0.dist-info}/METADATA +2 -1
- {nonebot_plugin_osubot-6.24.1.dist-info → nonebot_plugin_osubot-6.25.0.dist-info}/RECORD +11 -12
- nonebot_plugin_osubot/draw/osu_preview_templates/index.html +0 -437
- {nonebot_plugin_osubot-6.24.1.dist-info → nonebot_plugin_osubot-6.25.0.dist-info}/WHEEL +0 -0
nonebot_plugin_osubot/api.py
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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>
|
nonebot_plugin_osubot/file.py
CHANGED
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
server_osz_path = await upload_file_stream_batch(bot, osz_path)
|
|
18
19
|
try:
|
|
19
|
-
await
|
|
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
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
452
|
-
nonebot_plugin_osubot-6.
|
|
453
|
-
nonebot_plugin_osubot-6.
|
|
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>
|
|
File without changes
|