koishi-plugin-music-to-voice 1.0.0 → 1.0.1

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.
package/dist/index.mjs CHANGED
@@ -1,366 +1,353 @@
1
1
  // src/index.ts
2
- import { Schema, Logger, h } from "koishi";
3
- import axios from "axios";
4
- import fs from "fs";
5
- import path from "path";
2
+ import { Schema, h, Logger, isNullable } from "koishi";
6
3
  import os from "os";
4
+ import fs from "fs";
7
5
  import crypto from "crypto";
8
- var name = "music-voice-pro";
6
+ import path from "path";
7
+ import { pathToFileURL } from "url";
8
+ var name = "music-to-voice";
9
9
  var logger = new Logger(name);
10
- var Config = Schema.object({
11
- commandName: Schema.string().default("\u542C\u6B4C").description("\u6307\u4EE4\u540D\u79F0"),
12
- commandAlias: Schema.string().default("music").description("\u6307\u4EE4\u522B\u540D"),
13
- apiBase: Schema.string().default("https://music-api.gdstudio.xyz/api.php").description("\u97F3\u4E50 API \u5730\u5740\uFF08GD\u97F3\u4E50\u53F0 API\uFF09"),
14
- source: Schema.union([
15
- Schema.const("netease").description("\u7F51\u6613\u4E91"),
16
- Schema.const("tencent").description("QQ\u97F3\u4E50"),
17
- Schema.const("kugou").description("\u9177\u72D7"),
18
- Schema.const("kuwo").description("\u9177\u6211"),
19
- Schema.const("migu").description("\u54AA\u5495"),
20
- Schema.const("baidu").description("\u767E\u5EA6")
21
- ]).default("netease").description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09"),
22
- searchListCount: Schema.number().min(5).max(50).default(20).description("\u641C\u7D22\u5217\u8868\u6570\u91CF"),
23
- waitForTimeout: Schema.number().min(10).max(180).default(45).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09"),
24
- nextPageCommand: Schema.string().default("\u4E0B\u4E00\u9875").description("\u4E0B\u4E00\u9875\u6307\u4EE4"),
25
- prevPageCommand: Schema.string().default("\u4E0A\u4E00\u9875").description("\u4E0A\u4E00\u9875\u6307\u4EE4"),
26
- exitCommandList: Schema.array(String).default(["0", "\u4E0D\u542C\u4E86", "\u9000\u51FA"]).description("\u9000\u51FA\u6307\u4EE4\u5217\u8868"),
27
- menuExitCommandTip: Schema.boolean().default(false).description("\u662F\u5426\u5728\u6B4C\u5355\u672B\u5C3E\u63D0\u793A\u9000\u51FA\u6307\u4EE4"),
28
- imageMode: Schema.boolean().default(false).description("\u56FE\u7247\u6B4C\u5355\u6A21\u5F0F\uFF08\u53EF\u9009\uFF1A\u9700\u8981 puppeteer \u63D2\u4EF6\uFF0C\u5F53\u524D\u4EC5\u4FDD\u7559\u5F00\u5173\uFF09"),
29
- sendAs: Schema.union([
30
- Schema.const("record").description("\u8BED\u97F3 record"),
31
- Schema.const("audio").description("\u97F3\u9891 audio")
32
- ]).default("record").description("\u53D1\u9001\u7C7B\u578B"),
33
- forceTranscode: Schema.boolean().default(true).description("\u662F\u5426\u5F3A\u5236\u8F6C\u7801\u4E3A silk\uFF08\u9700\u8981 ffmpeg + silk \u63D2\u4EF6\uFF09"),
34
- tempDir: Schema.string().default(path.join(os.tmpdir(), "koishi-music-voice")).description("\u4E34\u65F6\u76EE\u5F55"),
35
- cacheMinutes: Schema.number().min(0).max(1440).default(120).description("\u7F13\u5B58\u65F6\u957F\uFF08\u5206\u949F\uFF0C0=\u4E0D\u7F13\u5B58\uFF09"),
36
- generationTip: Schema.string().default("\u751F\u6210\u8BED\u97F3\u4E2D...").description("\u7528\u6237\u9009\u6B4C\u540E\u63D0\u793A"),
37
- recallSearchMenuMessage: Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u6B4C\u5355\u6D88\u606F"),
38
- recallTipMessage: Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u751F\u6210\u63D0\u793A\u6D88\u606F"),
39
- recallUserSelectMessage: Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u7528\u6237\u8F93\u5165\u7684\u5E8F\u53F7\u6D88\u606F"),
40
- recallVoiceMessage: Schema.boolean().default(false).description("\u64A4\u56DE\uFF1A\u8BED\u97F3\u6D88\u606F"),
41
- loggerinfo: Schema.boolean().default(false).description("\u65E5\u5FD7\u8C03\u8BD5\u6A21\u5F0F")
42
- }).description("\u70B9\u6B4C\u8BED\u97F3\uFF08\u652F\u6301\u7FFB\u9875 + \u53EF\u9009 silk/ffmpeg\uFF09");
43
- function safeText(x) {
44
- return typeof x === "string" ? x : x == null ? "" : String(x);
45
- }
46
- function normalizeSearchList(data) {
47
- const arr = Array.isArray(data) ? data : Array.isArray(data?.result) ? data.result : Array.isArray(data?.data) ? data.data : Array.isArray(data?.songs) ? data.songs : [];
48
- return arr.map((it) => {
49
- const id = safeText(it?.id ?? it?.songid ?? it?.rid ?? it?.hash ?? it?.mid);
50
- const name2 = safeText(it?.name ?? it?.songname ?? it?.title);
51
- const artist = safeText(it?.artist) || safeText(it?.singer) || safeText(it?.author) || (Array.isArray(it?.artists) ? it.artists.map((a) => safeText(a?.name)).filter(Boolean).join("/") : "");
52
- const album = safeText(it?.album ?? it?.albummid ?? it?.albumname);
53
- return { id, name: name2, artist, album };
54
- }).filter((x) => x.id && x.name);
55
- }
56
- function normalizeUrl(data) {
57
- return safeText(data?.url) || safeText(data?.data?.url) || safeText(data?.result?.url) || safeText(data?.data) || "";
58
- }
59
- function md5(s) {
60
- return crypto.createHash("md5").update(s).digest("hex");
61
- }
62
- function ensureDir(p) {
63
- fs.mkdirSync(p, { recursive: true });
10
+ var inject = {
11
+ required: ["http", "i18n"],
12
+ optional: ["puppeteer", "downloads", "ffmpeg", "silk"]
13
+ };
14
+ var usage = `
15
+ ---
16
+
17
+ \u672C\u63D2\u4EF6\u63D0\u4F9B\u201C\u70B9\u6B4C \u2192 \u5217\u8868\u9009\u5E8F\u53F7 \u2192 \u53D1\u9001\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\u201D\u7684\u97F3\u4E50\u805A\u5408\u80FD\u529B\u3002
18
+
19
+ \u6570\u636E\u6765\u6E90\uFF1A**GD\u97F3\u4E50\u53F0 API**\uFF08https://music-api.gdstudio.xyz/api.php\uFF09
20
+
21
+ ---
22
+
23
+ ## \u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
24
+
25
+ \u6240\u9700\u670D\u52A1\uFF1A
26
+
27
+ - puppeteer \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF0C\u7528\u4E8E\u56FE\u7247\u6B4C\u5355\uFF09
28
+
29
+ \u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF08\u6700\u7A33\uFF1A\u4E0B\u8F7D + \u8F6C\u7801\uFF09\uFF1A
30
+
31
+ - downloads \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
32
+ - ffmpeg \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF0C\u53EF\u80FD\u4F9D\u8D56 downloads\uFF09
33
+ - silk \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
34
+
35
+ ---
36
+ `;
37
+ var Config = Schema.intersect([
38
+ Schema.object({
39
+ commandName: Schema.string().default("\u542C\u6B4C").description("\u6307\u4EE4\u540D\u79F0"),
40
+ commandAlias: Schema.string().default("music2").description("\u6307\u4EE4\u522B\u540D\uFF08\u907F\u514D\u547D\u4EE4\u51B2\u7A81\uFF09")
41
+ }).description("\u57FA\u7840\u8BBE\u7F6E"),
42
+ Schema.object({
43
+ apiBase: Schema.string().default("https://music-api.gdstudio.xyz/api.php").description("GD\u97F3\u4E50\u53F0 API \u5730\u5740"),
44
+ source: Schema.union([
45
+ Schema.const("netease").description("\u6E901"),
46
+ Schema.const("tencent").description("\u6E902"),
47
+ Schema.const("kugou").description("\u6E903"),
48
+ Schema.const("kuwo").description("\u6E904"),
49
+ Schema.const("migu").description("\u6E905")
50
+ ]).default("kuwo").description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09"),
51
+ searchListCount: Schema.natural().min(5).max(50).default(20).description("\u6BCF\u9875\u663E\u793A\u6761\u6570")
52
+ }).description("API \u8BBE\u7F6E"),
53
+ Schema.object({
54
+ imageMode: Schema.boolean().default(false).description("\u56FE\u7247\u6B4C\u5355\u6A21\u5F0F\uFF08\u9700\u8981 puppeteer\uFF0C\u53EF\u9009\u5B89\u88C5\uFF09"),
55
+ textColor: Schema.string().role("color").default("rgba(255,255,255,1)").description("\u56FE\u7247\u6B4C\u5355\u6587\u5B57\u989C\u8272"),
56
+ backgroundColor: Schema.string().role("color").default("rgba(0,0,0,1)").description("\u56FE\u7247\u6B4C\u5355\u80CC\u666F\u989C\u8272")
57
+ }).description("\u6B4C\u5355\u6837\u5F0F"),
58
+ Schema.object({
59
+ nextPageCommand: Schema.string().default("\u4E0B\u4E00\u9875").description("\u4E0B\u4E00\u9875\u6307\u4EE4"),
60
+ prevPageCommand: Schema.string().default("\u4E0A\u4E00\u9875").description("\u4E0A\u4E00\u9875\u6307\u4EE4"),
61
+ exitCommandList: Schema.array(Schema.string()).default(["0", "\u9000\u51FA", "\u4E0D\u542C\u4E86"]).description("\u9000\u51FA\u6307\u4EE4\u5217\u8868"),
62
+ menuExitCommandTip: Schema.boolean().default(false).description("\u662F\u5426\u5728\u6B4C\u5355\u5C3E\u90E8\u63D0\u793A\u9000\u51FA\u6307\u4EE4"),
63
+ waitForTimeout: Schema.natural().min(5).max(180).default(45).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09"),
64
+ generationTip: Schema.string().default("\u751F\u6210\u8BED\u97F3\u4E2D\u2026").description("\u751F\u6210\u63D0\u793A\u6587\u672C")
65
+ }).description("\u4EA4\u4E92\u8BBE\u7F6E"),
66
+ Schema.object({
67
+ sendAs: Schema.union([
68
+ Schema.const("record").description("\u8BED\u97F3 record"),
69
+ Schema.const("audio").description("\u97F3\u9891 audio"),
70
+ Schema.const("file").description("\u6587\u4EF6 file")
71
+ ]).default("record").description("\u53D1\u9001\u7C7B\u578B"),
72
+ forceTranscode: Schema.boolean().default(true).description("\u6700\u7A33\u6A21\u5F0F\uFF1A\u4E0B\u8F7D + ffmpeg + silk \u8F6C\u7801\uFF08\u53EF\u9009\u4F9D\u8D56\uFF0C\u7F3A\u4F9D\u8D56\u81EA\u52A8\u964D\u7EA7\u76F4\u94FE\uFF09")
73
+ }).description("\u53D1\u9001\u8BBE\u7F6E"),
74
+ Schema.object({
75
+ recallMenu: Schema.boolean().default(true).description("\u64A4\u56DE\u6B4C\u5355\u6D88\u606F"),
76
+ recallGeneratingTip: Schema.boolean().default(true).description("\u64A4\u56DE\u751F\u6210\u63D0\u793A\u6D88\u606F"),
77
+ recallSongMessage: Schema.boolean().default(false).description("\u64A4\u56DE\u6B4C\u66F2\u6D88\u606F\uFF08\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\uFF09"),
78
+ recallTimeoutTip: Schema.boolean().default(true).description("\u64A4\u56DE\u8D85\u65F6\u63D0\u793A"),
79
+ recallExitTip: Schema.boolean().default(false).description("\u64A4\u56DE\u9000\u51FA\u63D0\u793A"),
80
+ recallInvalidTip: Schema.boolean().default(false).description("\u64A4\u56DE\u5E8F\u53F7\u9519\u8BEF\u63D0\u793A"),
81
+ recallFetchFailTip: Schema.boolean().default(true).description("\u64A4\u56DE\u83B7\u53D6\u5931\u8D25\u63D0\u793A")
82
+ }).description("\u64A4\u56DE\u8BBE\u7F6E"),
83
+ Schema.object({
84
+ enableRateLimit: Schema.boolean().default(false).description("\u662F\u5426\u5F00\u542F\u9891\u7387\u9650\u5236"),
85
+ rateLimitScope: Schema.union([
86
+ Schema.const("user").description("\u6309\u7528\u6237"),
87
+ Schema.const("channel").description("\u6309\u9891\u9053"),
88
+ Schema.const("platform").description("\u6309\u5E73\u53F0")
89
+ ]).default("user").description("\u9650\u5236\u8303\u56F4"),
90
+ rateLimitIntervalSec: Schema.natural().min(1).max(3600).default(60).description("\u95F4\u9694\u79D2\u6570")
91
+ }).description("\u9891\u7387\u9650\u5236")
92
+ ]);
93
+ function artistsToString(x) {
94
+ if (!x) return "";
95
+ if (Array.isArray(x)) return x.join(" / ");
96
+ return String(x);
64
97
  }
65
- async function tryRecall(session, messageId) {
98
+ async function safeDelete(session, messageId) {
66
99
  if (!messageId) return;
100
+ const channelId = session.channelId;
101
+ if (!channelId) return;
67
102
  try {
68
- await session.bot.deleteMessage(session.channelId, messageId);
103
+ await session.bot.deleteMessage(channelId, messageId);
69
104
  } catch {
70
105
  }
71
106
  }
72
- function hRecord(src) {
73
- return h("record", { src });
107
+ function buildRateKey(session, scope) {
108
+ if (scope === "platform") return session.platform;
109
+ if (scope === "channel") return `${session.platform}:${session.channelId ?? ""}`;
110
+ return `${session.platform}:${session.userId ?? ""}`;
74
111
  }
75
- function hAudio(src) {
76
- return h("audio", { src });
77
- }
78
- function buildMenuText(config, keyword, list, page) {
79
- const lines = [];
80
- lines.push(`NetEase Music:`);
81
- lines.push(`\u5173\u952E\u8BCD\uFF1A${keyword}`);
82
- lines.push(`\u97F3\u6E90\uFF1A${config.source} \u7B2C ${page} \u9875`);
83
- lines.push("");
84
- for (let i = 0; i < list.length; i++) {
85
- const it = list[i];
86
- const meta = [it.artist, it.album].filter(Boolean).join(" - ");
87
- lines.push(`${i + 1}. ${it.name}${meta ? ` -- ${meta}` : ""}`);
88
- }
89
- lines.push("");
90
- lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
91
- lines.push(`\u7FFB\u9875\uFF1A${config.prevPageCommand} / ${config.nextPageCommand}`);
92
- if (config.menuExitCommandTip) {
93
- lines.push(`\u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`);
94
- }
95
- lines.push("");
96
- lines.push("\u6570\u636E\u6765\u6E90\uFF1AGD\u97F3\u4E50\u53F0 API");
97
- return lines.join("\n");
98
- }
99
- async function apiSearch(config, keyword, page = 1) {
100
- const params = {
101
- types: "search",
102
- source: config.source,
103
- name: keyword,
104
- count: config.searchListCount,
105
- pages: page
106
- };
107
- const { data } = await axios.get(config.apiBase, { params, timeout: 15e3 });
108
- return normalizeSearchList(data);
109
- }
110
- async function apiGetSongUrl(config, id) {
111
- const params = { types: "url", source: config.source, id };
112
- const { data } = await axios.get(config.apiBase, { params, timeout: 15e3 });
113
- return normalizeUrl(data);
114
- }
115
- async function downloadToFile(url, filePath) {
116
- const res = await axios.get(url, { responseType: "stream", timeout: 3e4 });
117
- await new Promise((resolve, reject) => {
118
- const ws = fs.createWriteStream(filePath);
119
- res.data.pipe(ws);
120
- ws.on("finish", () => resolve());
121
- ws.on("error", reject);
112
+ async function gdSearch(ctx, config, keyword, page) {
113
+ const data = await ctx.http.get(config.apiBase, {
114
+ timeout: 15e3,
115
+ params: { types: "search", source: config.source, name: keyword, count: config.searchListCount, pages: page },
116
+ headers: { "user-agent": "koishi-music-to-voice" }
122
117
  });
118
+ if (!Array.isArray(data)) return [];
119
+ return data.map((x) => ({
120
+ id: String(x.id ?? x.url_id ?? ""),
121
+ name: String(x.name ?? ""),
122
+ artist: artistsToString(x.artist),
123
+ album: String(x.album ?? ""),
124
+ source: String(x.source ?? config.source)
125
+ })).filter((x) => x.id && x.name);
123
126
  }
124
- async function sendVoiceByUrl(session, config, audioUrl) {
125
- const seg = config.sendAs === "record" ? hRecord(audioUrl) : hAudio(audioUrl);
126
- const ids = await session.send(seg);
127
- return Array.isArray(ids) ? ids[0] : ids;
128
- }
129
- async function sendVoiceByFile(session, config, absPath) {
130
- const url = `file://${absPath.replace(/\\/g, "/")}`;
131
- const seg = config.sendAs === "record" ? hRecord(url) : hAudio(url);
132
- const ids = await session.send(seg);
133
- return Array.isArray(ids) ? ids[0] : ids;
127
+ async function gdUrl(ctx, config, item) {
128
+ const data = await ctx.http.get(config.apiBase, {
129
+ timeout: 15e3,
130
+ params: { types: "url", source: item.source || config.source, id: item.id },
131
+ headers: { "user-agent": "koishi-music-to-voice" }
132
+ });
133
+ if (typeof data === "string") return data;
134
+ if (data?.url) return String(data.url);
135
+ if (data?.data?.url) return String(data.data.url);
136
+ return "";
134
137
  }
135
- async function buildSilkIfPossible(ctx, config, audioUrl, cacheKey) {
136
- const hasFfmpeg = !!ctx.ffmpeg;
137
- const hasSilk = !!ctx.silk;
138
- if (!hasFfmpeg || !hasSilk) return null;
139
- ensureDir(config.tempDir);
140
- const silkPath = path.join(config.tempDir, `${cacheKey}.silk`);
141
- if (config.cacheMinutes > 0 && fs.existsSync(silkPath)) {
142
- return silkPath;
143
- }
144
- const rawPath = path.join(config.tempDir, `${cacheKey}.src`);
145
- await downloadToFile(audioUrl, rawPath);
146
- const wavPath = path.join(config.tempDir, `${cacheKey}.wav`);
147
- const ffmpeg = ctx.ffmpeg;
138
+ async function downloadToTemp(ctx, src) {
148
139
  try {
149
- if (typeof ffmpeg.convert === "function") {
150
- await ffmpeg.convert(rawPath, wavPath, {
151
- format: "wav",
152
- audioChannels: 1,
153
- audioFrequency: 24e3
154
- });
155
- } else if (typeof ffmpeg.exec === "function") {
156
- await ffmpeg.exec(["-y", "-i", rawPath, "-ac", "1", "-ar", "24000", wavPath]);
157
- } else {
158
- throw new Error("ffmpeg service API not recognized");
159
- }
140
+ const file = await ctx.http.file(src);
141
+ const ext = ".mp3";
142
+ const fp = path.join(os.tmpdir(), `music_${crypto.randomBytes(8).toString("hex")}${ext}`);
143
+ fs.writeFileSync(fp, Buffer.from(file.data));
144
+ return fp;
160
145
  } catch (e) {
161
- throw new Error(`ffmpeg \u8F6C\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
146
+ return null;
162
147
  }
163
- const silk = ctx.silk;
148
+ }
149
+ async function tryTranscode(ctx, localPath) {
150
+ const anyCtx = ctx;
151
+ if (!anyCtx.ffmpeg || !anyCtx.silk) return localPath;
152
+ const wav = localPath.replace(/\.\w+$/, "") + ".wav";
153
+ const silk = localPath.replace(/\.\w+$/, "") + ".silk";
164
154
  try {
165
- if (typeof silk.encode === "function") {
166
- await silk.encode(wavPath, silkPath, { rate: 24e3 });
167
- } else if (typeof silk.encodeWav === "function") {
168
- await silk.encodeWav(wavPath, silkPath, { rate: 24e3 });
169
- } else {
170
- throw new Error("silk service API not recognized");
171
- }
172
- } catch (e) {
173
- throw new Error(`silk \u7F16\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
155
+ await anyCtx.ffmpeg.convert(localPath, wav);
156
+ await anyCtx.silk.encode(wav, silk);
157
+ return silk;
158
+ } catch {
159
+ return localPath;
174
160
  } finally {
175
161
  try {
176
- fs.unlinkSync(rawPath);
177
- } catch {
178
- }
179
- try {
180
- fs.unlinkSync(wavPath);
162
+ fs.existsSync(wav) && fs.unlinkSync(wav);
181
163
  } catch {
182
164
  }
183
165
  }
184
- return silkPath;
185
166
  }
186
- function logDepsHint(ctx) {
187
- const hasPuppeteer = !!ctx.puppeteer;
188
- const hasFfmpeg = !!ctx.ffmpeg;
189
- const hasSilk = !!ctx.silk;
190
- const hasDownloads = !!ctx.downloads;
191
- logger.info("\u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A");
192
- logger.info(`- puppeteer\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasPuppeteer ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
193
- logger.info("\u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF1A");
194
- logger.info(`- ffmpeg\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF08\u6B64\u670D\u52A1\u53EF\u80FD\u989D\u5916\u4F9D\u8D56downloads\u670D\u52A1\uFF09\uFF1A${hasFfmpeg ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
195
- logger.info(`- silk\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasSilk ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
196
- logger.info(`- downloads\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasDownloads ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
197
- logger.info("Music API \u51FA\u5904\uFF1AGD\u97F3\u4E50\u53F0 API\uFF08https://music-api.gdstudio.xyz/api.php\uFF09");
167
+ async function renderListImage(ctx, html) {
168
+ const anyCtx = ctx;
169
+ if (!anyCtx.puppeteer) return null;
170
+ const page = await anyCtx.puppeteer.page();
171
+ await page.setContent(html);
172
+ const el = await page.$("#song-list");
173
+ if (!el) return null;
174
+ const buf = await el.screenshot({});
175
+ await page.close();
176
+ return buf;
198
177
  }
199
- function apply(ctx, config) {
200
- if (config.loggerinfo) logger.level = Logger.DEBUG;
201
- logDepsHint(ctx);
202
- const pending = /* @__PURE__ */ new Map();
203
- function getKey(session) {
204
- return String(session?.channelId || "");
205
- }
206
- function isExit(input) {
207
- const t = input.trim();
208
- return config.exitCommandList.map((s) => s.trim()).includes(t);
209
- }
210
- function isExpired(state) {
211
- return Date.now() > state.expiresAt;
212
- }
213
- async function refreshMenu(session, state) {
214
- const list = await apiSearch(config, state.keyword, state.page);
215
- state.list = list;
216
- state.expiresAt = Date.now() + config.waitForTimeout * 1e3;
217
- if (config.recallSearchMenuMessage) {
218
- await tryRecall(session, state.menuMessageId);
219
- state.menuMessageId = void 0;
220
- }
221
- if (!config.recallSearchMenuMessage) {
222
- const text = buildMenuText(config, state.keyword, list, state.page);
223
- const ids = await session.send(text);
224
- state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
225
- } else {
226
- await session.send(`\u5DF2\u7FFB\u5230\u7B2C ${state.page} \u9875\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
227
- }
178
+ function makeListText(config, list, page) {
179
+ const start = (page - 1) * config.searchListCount;
180
+ const lines = list.map((s, i) => `${start + i + 1}. ${s.name} -- ${s.artist}${s.album ? " -- " + s.album : ""}`);
181
+ let tail = `
182
+
183
+ \u7FFB\u9875\uFF1A${config.prevPageCommand} / ${config.nextPageCommand}`;
184
+ if (config.menuExitCommandTip && config.exitCommandList?.length) {
185
+ tail += `
186
+ \u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`;
228
187
  }
229
- async function handlePick(session, state, pickIndex) {
230
- const item = state.list[pickIndex];
231
- if (!item) {
232
- await session.send(`\u5E8F\u53F7\u65E0\u6548\uFF0C\u8BF7\u8F93\u5165 1-${state.list.length}\uFF0C\u6216\u8F93\u5165 ${config.exitCommandList.join("/")} \u9000\u51FA\u3002`);
233
- return;
234
- }
235
- let tipId;
236
- if (!config.recallTipMessage && config.generationTip?.trim()) {
237
- const ids = await session.send(config.generationTip);
238
- tipId = Array.isArray(ids) ? ids[0] : ids;
239
- }
240
- let songUrl = "";
241
- try {
242
- songUrl = await apiGetSongUrl(config, item.id);
243
- if (!songUrl) throw new Error("empty url");
244
- } catch {
245
- await session.send("\u83B7\u53D6\u6B4C\u66F2\u76F4\u94FE\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\uFF0C\u6216\u66F4\u6362\u97F3\u6E90\u3002");
246
- return;
188
+ return `\u97F3\u4E50\u5217\u8868\uFF1A
189
+ ${lines.join("\n")}${tail}
190
+
191
+ \u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF1A`;
192
+ }
193
+ function apply(ctx, config) {
194
+ const lastUse = /* @__PURE__ */ new Map();
195
+ ctx.command(`${config.commandName} <keyword:text>`, "\u97F3\u4E50\u805A\u5408\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3").alias(config.commandAlias).action(async (argv, keyword) => {
196
+ const session = argv.session;
197
+ const options = argv.options ?? {};
198
+ if (!session) return;
199
+ keyword = (keyword ?? "").trim();
200
+ if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
201
+ if (config.enableRateLimit) {
202
+ const scope = config.rateLimitScope ?? "user";
203
+ const key = buildRateKey(session, scope);
204
+ const now = Date.now();
205
+ const last = lastUse.get(key) ?? 0;
206
+ const interval = (config.rateLimitIntervalSec ?? 60) * 1e3;
207
+ if (now - last < interval) {
208
+ const remain = Math.ceil((interval - (now - last)) / 1e3);
209
+ return `\u64CD\u4F5C\u592A\u9891\u7E41\uFF0C\u8BF7 ${remain} \u79D2\u540E\u518D\u8BD5\u3002`;
210
+ }
211
+ lastUse.set(key, now);
247
212
  }
248
- const cacheKey = md5(`${config.source}:${item.id}`);
249
- let voiceId;
250
- try {
251
- const silkPath = await buildSilkIfPossible(ctx, config, songUrl, cacheKey);
252
- if (silkPath) {
253
- voiceId = await sendVoiceByFile(session, config, silkPath);
254
- } else {
255
- if (config.forceTranscode) {
256
- await session.send(
257
- `\u5F53\u524D\u914D\u7F6E\u4E3A\u3010\u5F3A\u5236 silk \u8F6C\u7801\u3011\u4F46\u672A\u68C0\u6D4B\u5230 ffmpeg/silk \u670D\u52A1\u3002
258
- \u8BF7\u5728 Koishi \u63D2\u4EF6\u5E02\u573A\u5B89\u88C5\u5E76\u542F\u7528\uFF1Affmpeg\u3001silk\uFF08\u53EF\u80FD\u8FD8\u9700\u8981 downloads\uFF09\u3002`
259
- );
213
+ let page = 1;
214
+ let menuMsgId = null;
215
+ while (true) {
216
+ let list = [];
217
+ try {
218
+ list = await gdSearch(ctx, config, keyword, page);
219
+ } catch (e) {
220
+ logger.warn(`[search] ${String(e?.message || e)}`);
221
+ const ids = await session.send("\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
222
+ const mid = Array.isArray(ids) ? ids[0] : ids;
223
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
224
+ return;
225
+ }
226
+ if (!list.length) {
227
+ const ids = await session.send("\u6CA1\u6709\u641C\u7D22\u5230\u7ED3\u679C\uFF0C\u8BF7\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002");
228
+ const mid = Array.isArray(ids) ? ids[0] : ids;
229
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
230
+ return;
231
+ }
232
+ if (menuMsgId && config.recallMenu) await safeDelete(session, menuMsgId);
233
+ if (config.imageMode) {
234
+ const text2 = makeListText(config, list, page).replace(/\n/g, "<br/>");
235
+ const html = `<!doctype html><html><head><meta charset="utf-8"/><style>
236
+ body{margin:0;background:${config.backgroundColor};color:${config.textColor};font-size:16px;}
237
+ #song-list{padding:18px;white-space:nowrap;}
238
+ </style></head><body><div id="song-list">${text2}</div></body></html>`;
239
+ const buf = await renderListImage(ctx, html);
240
+ if (!buf) {
241
+ const ids2 = await session.send("\u56FE\u7247\u6B4C\u5355\u751F\u6210\u5931\u8D25\uFF1A\u672A\u5B89\u88C5 puppeteer \u670D\u52A1\u6216\u5176\u4E0D\u53EF\u7528\u3002");
242
+ const mid = Array.isArray(ids2) ? ids2[0] : ids2;
243
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
260
244
  return;
261
245
  }
262
- voiceId = await sendVoiceByUrl(session, config, songUrl);
246
+ const ids = await session.send([h.image(buf, "image/png")]);
247
+ menuMsgId = Array.isArray(ids) ? ids[0] : ids;
248
+ } else {
249
+ const txt = makeListText(config, list, page);
250
+ const ids = await session.send(txt);
251
+ menuMsgId = Array.isArray(ids) ? ids[0] : ids;
263
252
  }
264
- } catch (e) {
265
- await session.send(`\u751F\u6210\u8BED\u97F3\u5931\u8D25\uFF1A${e?.message || String(e)}
266
- \u8BF7\u68C0\u67E5 ffmpeg/silk \u63D2\u4EF6\u662F\u5426\u542F\u7528\uFF0C\u6216\u5173\u95ED\u201C\u5F3A\u5236\u8F6C\u7801\u201D\u3002`);
267
- return;
268
- } finally {
269
- state.tipMessageId = tipId;
270
- state.voiceMessageId = voiceId;
271
- }
272
- if (config.recallSearchMenuMessage) await tryRecall(session, state.menuMessageId);
273
- if (config.recallTipMessage) await tryRecall(session, state.tipMessageId);
274
- if (config.recallVoiceMessage) await tryRecall(session, state.voiceMessageId);
275
- pending.delete(getKey(session));
276
- }
277
- ctx.command(`${config.commandName} <keyword:text>`, "\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3\uFF08GD\u97F3\u4E50\u53F0 API\uFF09").alias(config.commandAlias).action(async ({ session }, keyword) => {
278
- if (!session) return;
279
- keyword = (keyword || "").trim();
280
- if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
281
- const userId = session.userId;
282
- const channelId = session.channelId;
283
- if (!userId || !channelId) {
284
- await session.send("\u5F53\u524D\u9002\u914D\u5668\u672A\u63D0\u4F9B userId/channelId\uFF0C\u65E0\u6CD5\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\u3002");
285
- return;
286
- }
287
- let list = [];
288
- try {
289
- list = await apiSearch(config, keyword, 1);
290
- } catch {
291
- return "\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
292
- }
293
- if (!list.length) return "\u6CA1\u6709\u641C\u5230\u7ED3\u679C\uFF0C\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002";
294
- const state = {
295
- userId,
296
- channelId,
297
- guildId: session.guildId,
298
- keyword,
299
- page: 1,
300
- list,
301
- expiresAt: Date.now() + config.waitForTimeout * 1e3
302
- };
303
- if (!config.recallSearchMenuMessage) {
304
- const text = buildMenuText(config, keyword, list, 1);
305
- const ids = await session.send(text);
306
- state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
307
- } else {
308
- await session.send(`\u5DF2\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09\uFF0C\u6216\u53D1\u9001\u201C${config.nextPageCommand}/${config.prevPageCommand}\u201D\u7FFB\u9875\u3002`);
309
- }
310
- pending.set(channelId, state);
311
- });
312
- ctx.middleware(async (session, next) => {
313
- const key = getKey(session);
314
- if (!key) return next();
315
- const state = pending.get(key);
316
- if (!state) return next();
317
- if (session.userId !== state.userId) return next();
318
- if (isExpired(state)) {
319
- pending.delete(key);
320
- return next();
321
- }
322
- const content = (session.content || "").trim();
323
- if (!content) return next();
324
- if (isExit(content)) {
325
- if (config.recallSearchMenuMessage) await tryRecall(session, state.menuMessageId);
326
- pending.delete(key);
327
- if (!config.recallUserSelectMessage) await session.send("\u5DF2\u9000\u51FA\u9009\u6B4C\u3002");
328
- return;
329
- }
330
- if (content === config.nextPageCommand) {
331
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
332
- state.page += 1;
253
+ const input = await session.prompt(
254
+ (s) => {
255
+ const els = s.elements ?? [];
256
+ return h.select(els, "text").join("");
257
+ },
258
+ { timeout: config.waitForTimeout * 1e3 }
259
+ );
260
+ if (isNullable(input)) {
261
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
262
+ const ids = await session.send("\u8F93\u5165\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u70B9\u6B4C\u3002");
263
+ const mid = Array.isArray(ids) ? ids[0] : ids;
264
+ if (config.recallTimeoutTip) await safeDelete(session, mid);
265
+ return;
266
+ }
267
+ const text = String(input).trim();
268
+ if (!text) continue;
269
+ if (config.exitCommandList.includes(text)) {
270
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
271
+ const ids = await session.send("\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
272
+ const mid = Array.isArray(ids) ? ids[0] : ids;
273
+ if (config.recallExitTip) await safeDelete(session, mid);
274
+ return;
275
+ }
276
+ if (text === config.nextPageCommand) {
277
+ page += 1;
278
+ continue;
279
+ }
280
+ if (text === config.prevPageCommand) {
281
+ page = Math.max(1, page - 1);
282
+ continue;
283
+ }
284
+ if (!/^\d+$/.test(text)) continue;
285
+ const idx = parseInt(text, 10);
286
+ const start = (page - 1) * config.searchListCount;
287
+ const local = idx - start - 1;
288
+ if (local < 0 || local >= list.length) {
289
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
290
+ const ids = await session.send("\u5E8F\u53F7\u8F93\u5165\u9519\u8BEF\uFF0C\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
291
+ const mid = Array.isArray(ids) ? ids[0] : ids;
292
+ if (config.recallInvalidTip) await safeDelete(session, mid);
293
+ return;
294
+ }
295
+ const chosen = list[local];
296
+ let tipMsgId = null;
297
+ {
298
+ const ids = await session.send(config.generationTip);
299
+ tipMsgId = Array.isArray(ids) ? ids[0] : ids;
300
+ }
301
+ let playUrl = "";
333
302
  try {
334
- await refreshMenu(session, state);
335
- } catch {
336
- state.page -= 1;
337
- await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
303
+ playUrl = await gdUrl(ctx, config, chosen);
304
+ } catch (e) {
305
+ logger.warn(`[url] ${String(e?.message || e)}`);
338
306
  }
339
- return;
340
- }
341
- if (content === config.prevPageCommand) {
342
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
343
- if (state.page <= 1) {
344
- await session.send("\u5DF2\u7ECF\u662F\u7B2C\u4E00\u9875\u3002");
307
+ if (!playUrl) {
308
+ if (config.recallGeneratingTip) await safeDelete(session, tipMsgId);
309
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
310
+ const ids = await session.send("\u83B7\u53D6\u6B4C\u66F2\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
311
+ const mid = Array.isArray(ids) ? ids[0] : ids;
312
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
345
313
  return;
346
314
  }
347
- state.page -= 1;
348
315
  try {
349
- await refreshMenu(session, state);
350
- } catch {
351
- state.page += 1;
352
- await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
316
+ let finalSrc = playUrl;
317
+ if (config.forceTranscode) {
318
+ const localPath = await downloadToTemp(ctx, playUrl);
319
+ if (localPath) {
320
+ const out = await tryTranscode(ctx, localPath);
321
+ finalSrc = pathToFileURL(out).href;
322
+ }
323
+ }
324
+ let sendIds;
325
+ if (config.sendAs === "file") {
326
+ sendIds = await session.send([h.file(finalSrc)]);
327
+ } else if (config.sendAs === "audio") {
328
+ sendIds = await session.send([h.audio(finalSrc)]);
329
+ } else {
330
+ sendIds = await session.send([h("record", { src: finalSrc })]);
331
+ }
332
+ const songMsgId = Array.isArray(sendIds) ? sendIds[0] : sendIds;
333
+ if (config.recallSongMessage) await safeDelete(session, songMsgId);
334
+ } catch (e) {
335
+ logger.warn(`[send] ${String(e?.message || e)}`);
336
+ const ids = await session.send("\u53D1\u9001\u5931\u8D25\uFF08\u53EF\u80FD\u7F3A\u5C11\u8F6C\u7801\u4F9D\u8D56\u6216\u94FE\u63A5\u5931\u6548\uFF09\u3002");
337
+ const mid = Array.isArray(ids) ? ids[0] : ids;
338
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
339
+ } finally {
340
+ if (config.recallGeneratingTip) await safeDelete(session, tipMsgId);
341
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
353
342
  }
354
343
  return;
355
344
  }
356
- const n = Number(content);
357
- if (!Number.isInteger(n) || n < 1 || n > state.list.length) return next();
358
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
359
- await handlePick(session, state, n - 1);
360
345
  });
361
346
  }
362
347
  export {
363
348
  Config,
364
349
  apply,
365
- name
350
+ inject,
351
+ name,
352
+ usage
366
353
  };