koishi-plugin-music-to-voice 1.0.0
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/README.md +12 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +403 -0
- package/dist/index.mjs +366 -0
- package/package.json +66 -0
- package/src/index.ts +516 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
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";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
var name = "music-voice-pro";
|
|
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 });
|
|
64
|
+
}
|
|
65
|
+
async function tryRecall(session, messageId) {
|
|
66
|
+
if (!messageId) return;
|
|
67
|
+
try {
|
|
68
|
+
await session.bot.deleteMessage(session.channelId, messageId);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function hRecord(src) {
|
|
73
|
+
return h("record", { src });
|
|
74
|
+
}
|
|
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);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
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;
|
|
134
|
+
}
|
|
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;
|
|
148
|
+
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
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
throw new Error(`ffmpeg \u8F6C\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
|
|
162
|
+
}
|
|
163
|
+
const silk = ctx.silk;
|
|
164
|
+
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)}`);
|
|
174
|
+
} finally {
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(rawPath);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(wavPath);
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return silkPath;
|
|
185
|
+
}
|
|
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");
|
|
198
|
+
}
|
|
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
|
+
}
|
|
228
|
+
}
|
|
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;
|
|
247
|
+
}
|
|
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
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
voiceId = await sendVoiceByUrl(session, config, songUrl);
|
|
263
|
+
}
|
|
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;
|
|
333
|
+
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");
|
|
338
|
+
}
|
|
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");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
state.page -= 1;
|
|
348
|
+
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");
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
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
|
+
});
|
|
361
|
+
}
|
|
362
|
+
export {
|
|
363
|
+
Config,
|
|
364
|
+
apply,
|
|
365
|
+
name
|
|
366
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-music-to-voice",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "音乐聚合点播语音插件:搜索歌曲、翻页选择、序号点播,可选转码发送语音。数据来源:GD音乐台 API。",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"koishi",
|
|
16
|
+
"koishi-plugin",
|
|
17
|
+
"music",
|
|
18
|
+
"voice",
|
|
19
|
+
"aggregate"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "sdxcvbot <zhufuzhi1@gmail.com>",
|
|
23
|
+
"homepage": "https://github.com/sdxcvbot/koishi-plugin-music-to-voice#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/sdxcvbot/koishi-plugin-music-to-voice.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sdxcvbot/koishi-plugin-music-to-voice/issues"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"koishi": "^4.18.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"axios": "^1.7.7"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"koishi": "^4.18.0",
|
|
45
|
+
"tsup": "^8.2.4",
|
|
46
|
+
"typescript": "^5.4.5"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
50
|
+
"dev": "tsup src/index.ts --watch --format cjs,esm --dts",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"koishi": {
|
|
54
|
+
"description": {
|
|
55
|
+
"zh": "音乐聚合点播语音插件:搜索→列表→翻页→序号点播;可选 ffmpeg+silk 转码发送语音。数据来源:GD音乐台 API。"
|
|
56
|
+
},
|
|
57
|
+
"service": {
|
|
58
|
+
"optional": [
|
|
59
|
+
"puppeteer",
|
|
60
|
+
"downloads",
|
|
61
|
+
"ffmpeg",
|
|
62
|
+
"silk"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|