auralis-sdk 0.2.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 +90 -0
- package/dist/index.cjs +674 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +625 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
APIError: () => APIError,
|
|
34
|
+
AuralisError: () => AuralisError,
|
|
35
|
+
AuthError: () => AuthError,
|
|
36
|
+
Client: () => Client,
|
|
37
|
+
EndReason: () => EndReason,
|
|
38
|
+
LoadError: () => LoadError,
|
|
39
|
+
LoadType: () => LoadType,
|
|
40
|
+
LoopMode: () => LoopMode,
|
|
41
|
+
NotConnected: () => NotConnected,
|
|
42
|
+
Player: () => Player,
|
|
43
|
+
REST: () => REST,
|
|
44
|
+
RateLimited: () => RateLimited,
|
|
45
|
+
endReasonAdvances: () => endReasonAdvances
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/client.ts
|
|
50
|
+
var import_node_events = require("events");
|
|
51
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
52
|
+
|
|
53
|
+
// src/errors.ts
|
|
54
|
+
var AuralisError = class extends Error {
|
|
55
|
+
};
|
|
56
|
+
var AuthError = class extends AuralisError {
|
|
57
|
+
};
|
|
58
|
+
var RateLimited = class extends AuralisError {
|
|
59
|
+
constructor(message, retryAfter) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.retryAfter = retryAfter;
|
|
62
|
+
}
|
|
63
|
+
retryAfter;
|
|
64
|
+
};
|
|
65
|
+
var APIError = class extends AuralisError {
|
|
66
|
+
constructor(status, message) {
|
|
67
|
+
super(`HTTP ${status}: ${message}`);
|
|
68
|
+
this.status = status;
|
|
69
|
+
}
|
|
70
|
+
status;
|
|
71
|
+
};
|
|
72
|
+
var NotConnected = class extends AuralisError {
|
|
73
|
+
};
|
|
74
|
+
var LoadError = class extends AuralisError {
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/types.ts
|
|
78
|
+
var LoopMode = /* @__PURE__ */ ((LoopMode2) => {
|
|
79
|
+
LoopMode2["Off"] = "off";
|
|
80
|
+
LoopMode2["Track"] = "track";
|
|
81
|
+
LoopMode2["Queue"] = "queue";
|
|
82
|
+
return LoopMode2;
|
|
83
|
+
})(LoopMode || {});
|
|
84
|
+
var LoadType = /* @__PURE__ */ ((LoadType2) => {
|
|
85
|
+
LoadType2["Track"] = "track";
|
|
86
|
+
LoadType2["Playlist"] = "playlist";
|
|
87
|
+
LoadType2["Search"] = "search";
|
|
88
|
+
LoadType2["Empty"] = "empty";
|
|
89
|
+
LoadType2["Error"] = "error";
|
|
90
|
+
return LoadType2;
|
|
91
|
+
})(LoadType || {});
|
|
92
|
+
var EndReason = /* @__PURE__ */ ((EndReason2) => {
|
|
93
|
+
EndReason2["Finished"] = "FINISHED";
|
|
94
|
+
EndReason2["LoadFailed"] = "LOAD_FAILED";
|
|
95
|
+
EndReason2["Stopped"] = "STOPPED";
|
|
96
|
+
EndReason2["Replaced"] = "REPLACED";
|
|
97
|
+
EndReason2["Cleanup"] = "CLEANUP";
|
|
98
|
+
return EndReason2;
|
|
99
|
+
})(EndReason || {});
|
|
100
|
+
function endReasonAdvances(reason) {
|
|
101
|
+
return reason === "FINISHED" /* Finished */ || reason === "LOAD_FAILED" /* LoadFailed */;
|
|
102
|
+
}
|
|
103
|
+
function trackFromJson(d) {
|
|
104
|
+
return {
|
|
105
|
+
encoded: d.encoded,
|
|
106
|
+
identifier: d.identifier ?? "",
|
|
107
|
+
title: d.title ?? "",
|
|
108
|
+
author: d.author ?? "",
|
|
109
|
+
lengthMs: d.length_ms ?? 0,
|
|
110
|
+
isStream: d.is_stream ?? false,
|
|
111
|
+
uri: d.uri ?? void 0,
|
|
112
|
+
sourceName: d.source_name ?? "",
|
|
113
|
+
artworkUrl: d.artwork_url ?? void 0,
|
|
114
|
+
isrc: d.isrc ?? void 0,
|
|
115
|
+
extras: {}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function loadResultFromJson(d) {
|
|
119
|
+
return {
|
|
120
|
+
loadType: d.load_type ?? "empty",
|
|
121
|
+
tracks: (d.tracks ?? []).map(trackFromJson),
|
|
122
|
+
playlistName: d.playlist_name ?? void 0,
|
|
123
|
+
error: d.error ?? void 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function playerStateFromJson(d) {
|
|
127
|
+
return {
|
|
128
|
+
connected: d.connected ?? false,
|
|
129
|
+
playing: d.playing ?? false,
|
|
130
|
+
paused: d.paused ?? false,
|
|
131
|
+
positionMs: d.position_ms ?? 0,
|
|
132
|
+
volume: d.volume ?? 100,
|
|
133
|
+
track: d.track ? trackFromJson(d.track) : void 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/player.ts
|
|
138
|
+
var Player = class {
|
|
139
|
+
constructor(host, guildId) {
|
|
140
|
+
this.host = host;
|
|
141
|
+
this.guildId = guildId;
|
|
142
|
+
}
|
|
143
|
+
host;
|
|
144
|
+
guildId;
|
|
145
|
+
queue = [];
|
|
146
|
+
history = [];
|
|
147
|
+
current = null;
|
|
148
|
+
/** Cap on retained history so a long-lived player can't leak memory. Queue-loop
|
|
149
|
+
* still works; it just restores the most recent `historyLimit` tracks. */
|
|
150
|
+
historyLimit = 200;
|
|
151
|
+
loop = "off" /* Off */;
|
|
152
|
+
volume = 100;
|
|
153
|
+
paused = false;
|
|
154
|
+
state = {
|
|
155
|
+
connected: false,
|
|
156
|
+
playing: false,
|
|
157
|
+
paused: false,
|
|
158
|
+
positionMs: 0,
|
|
159
|
+
volume: 100
|
|
160
|
+
};
|
|
161
|
+
/** Optional async hook used when the queue and loop are exhausted. */
|
|
162
|
+
autoplay = null;
|
|
163
|
+
created = false;
|
|
164
|
+
voiceServer = null;
|
|
165
|
+
voiceState = null;
|
|
166
|
+
// --- voice plumbing ---------------------------------------------------
|
|
167
|
+
handleVoiceServerUpdate(data) {
|
|
168
|
+
this.voiceServer = { token: data.token, endpoint: data.endpoint };
|
|
169
|
+
void this.maybeStart();
|
|
170
|
+
}
|
|
171
|
+
handleVoiceStateUpdate(data) {
|
|
172
|
+
if (data.channel_id === null || data.channel_id === void 0) {
|
|
173
|
+
this.voiceState = null;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.voiceState = {
|
|
177
|
+
session_id: data.session_id,
|
|
178
|
+
channel_id: Number(data.channel_id),
|
|
179
|
+
user_id: Number(data.user_id)
|
|
180
|
+
};
|
|
181
|
+
void this.maybeStart();
|
|
182
|
+
}
|
|
183
|
+
get connected() {
|
|
184
|
+
return this.voiceServer !== null && this.voiceState !== null;
|
|
185
|
+
}
|
|
186
|
+
voicePayload() {
|
|
187
|
+
return { ...this.voiceServer, ...this.voiceState, guild_id: this.guildId };
|
|
188
|
+
}
|
|
189
|
+
async maybeStart() {
|
|
190
|
+
if (this.connected && this.current === null && this.queue.length > 0) {
|
|
191
|
+
await this.advance();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// --- public commands --------------------------------------------------
|
|
195
|
+
async play(query, requester) {
|
|
196
|
+
const tracks = await this.resolve(query);
|
|
197
|
+
for (const t of tracks) {
|
|
198
|
+
if (requester !== void 0) t.extras.requester = requester;
|
|
199
|
+
this.queue.push(t);
|
|
200
|
+
}
|
|
201
|
+
if (this.current === null && this.connected) await this.advance();
|
|
202
|
+
return tracks;
|
|
203
|
+
}
|
|
204
|
+
async add(query, requester) {
|
|
205
|
+
const tracks = await this.resolve(query);
|
|
206
|
+
for (const t of tracks) {
|
|
207
|
+
if (requester !== void 0) t.extras.requester = requester;
|
|
208
|
+
this.queue.push(t);
|
|
209
|
+
}
|
|
210
|
+
return tracks;
|
|
211
|
+
}
|
|
212
|
+
async skip() {
|
|
213
|
+
await this.advance(true);
|
|
214
|
+
}
|
|
215
|
+
async stop() {
|
|
216
|
+
this.queue = [];
|
|
217
|
+
this.current = null;
|
|
218
|
+
if (this.created) await this.host.rest.stop(this.guildId);
|
|
219
|
+
}
|
|
220
|
+
async pause() {
|
|
221
|
+
await this.setPausedInternal(true);
|
|
222
|
+
}
|
|
223
|
+
async resume() {
|
|
224
|
+
await this.setPausedInternal(false);
|
|
225
|
+
}
|
|
226
|
+
async setPause(paused) {
|
|
227
|
+
await this.setPausedInternal(paused);
|
|
228
|
+
}
|
|
229
|
+
async seek(positionMs) {
|
|
230
|
+
if (this.created) await this.host.rest.seek(this.guildId, positionMs);
|
|
231
|
+
}
|
|
232
|
+
async setVolume(volume) {
|
|
233
|
+
this.volume = Math.max(0, Math.min(1e3, volume));
|
|
234
|
+
if (this.created) await this.host.rest.setVolume(this.guildId, this.volume);
|
|
235
|
+
}
|
|
236
|
+
setLoop(mode) {
|
|
237
|
+
this.loop = mode;
|
|
238
|
+
}
|
|
239
|
+
shuffle() {
|
|
240
|
+
for (let i = this.queue.length - 1; i > 0; i--) {
|
|
241
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
242
|
+
[this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Apply a raw ffmpeg `-af` filtergraph (e.g. `"bass=g=8"`); `null` clears. */
|
|
246
|
+
async setFilters(audioGraph) {
|
|
247
|
+
if (this.created) await this.host.rest.setFilters(this.guildId, audioGraph);
|
|
248
|
+
}
|
|
249
|
+
remove(index) {
|
|
250
|
+
return this.queue.splice(index, 1)[0];
|
|
251
|
+
}
|
|
252
|
+
clear() {
|
|
253
|
+
this.queue = [];
|
|
254
|
+
}
|
|
255
|
+
/** Reorder the queue: move the track at `from` to `to`. No-op on bad indices. */
|
|
256
|
+
move(from, to) {
|
|
257
|
+
if (from < 0 || from >= this.queue.length || to < 0 || to >= this.queue.length) return;
|
|
258
|
+
const [t] = this.queue.splice(from, 1);
|
|
259
|
+
this.queue.splice(to, 0, t);
|
|
260
|
+
}
|
|
261
|
+
/** Resolve `query` and insert it at `index` (clamped) instead of appending. */
|
|
262
|
+
async insert(index, query, requester) {
|
|
263
|
+
const tracks = await this.resolve(query);
|
|
264
|
+
for (const t of tracks) if (requester !== void 0) t.extras.requester = requester;
|
|
265
|
+
const at = Math.max(0, Math.min(index, this.queue.length));
|
|
266
|
+
this.queue.splice(at, 0, ...tracks);
|
|
267
|
+
if (this.current === null && this.connected) await this.advance();
|
|
268
|
+
return tracks;
|
|
269
|
+
}
|
|
270
|
+
/** Batch-enqueue several queries/tracks in order. Failed resolves are skipped. */
|
|
271
|
+
async addMany(queries, requester) {
|
|
272
|
+
const added = [];
|
|
273
|
+
for (const q of queries) {
|
|
274
|
+
try {
|
|
275
|
+
const tracks = await this.resolve(q);
|
|
276
|
+
for (const t of tracks) {
|
|
277
|
+
if (requester !== void 0) t.extras.requester = requester;
|
|
278
|
+
this.queue.push(t);
|
|
279
|
+
added.push(t);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (this.current === null && this.connected && added.length > 0) await this.advance();
|
|
285
|
+
return added;
|
|
286
|
+
}
|
|
287
|
+
/** Skip forward to queue position `index`, discarding the ones in between. */
|
|
288
|
+
async jump(index) {
|
|
289
|
+
if (index < 0 || index >= this.queue.length) return;
|
|
290
|
+
const skipped = this.queue.splice(0, index);
|
|
291
|
+
for (const t of skipped) this.pushHistory(t);
|
|
292
|
+
await this.advance(true);
|
|
293
|
+
}
|
|
294
|
+
/** Replay the previous track (from history), pushing the current one back on. */
|
|
295
|
+
async previous() {
|
|
296
|
+
const prev = this.history.pop();
|
|
297
|
+
if (!prev) return;
|
|
298
|
+
if (this.current !== null) this.queue.unshift(this.current);
|
|
299
|
+
this.queue.unshift(prev);
|
|
300
|
+
await this.advance(true);
|
|
301
|
+
}
|
|
302
|
+
/** Convenience EQ: bass boost in dB (0 clears). Built on {@link setFilters}. */
|
|
303
|
+
async setBassBoost(gainDb) {
|
|
304
|
+
await this.setFilters(gainDb === 0 ? null : `bass=g=${gainDb}`);
|
|
305
|
+
}
|
|
306
|
+
/** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal. */
|
|
307
|
+
async setSpeed(rate) {
|
|
308
|
+
const r = Math.max(0.5, Math.min(2, rate));
|
|
309
|
+
await this.setFilters(r === 1 ? null : `atempo=${r},asetrate=48000*${r}`);
|
|
310
|
+
}
|
|
311
|
+
nowPlaying() {
|
|
312
|
+
return this.current;
|
|
313
|
+
}
|
|
314
|
+
async disconnect() {
|
|
315
|
+
this.queue = [];
|
|
316
|
+
this.current = null;
|
|
317
|
+
if (this.created) await this.host.rest.destroyPlayer(this.guildId);
|
|
318
|
+
this.created = false;
|
|
319
|
+
this.voiceServer = null;
|
|
320
|
+
this.voiceState = null;
|
|
321
|
+
this.host.dropPlayer(this.guildId);
|
|
322
|
+
}
|
|
323
|
+
// --- internals --------------------------------------------------------
|
|
324
|
+
/** Append to history, evicting the oldest past `historyLimit` (leak guard). */
|
|
325
|
+
pushHistory(track) {
|
|
326
|
+
this.history.push(track);
|
|
327
|
+
if (this.history.length > this.historyLimit) {
|
|
328
|
+
this.history.splice(0, this.history.length - this.historyLimit);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async resolve(query) {
|
|
332
|
+
if (typeof query !== "string") return [query];
|
|
333
|
+
const result = await this.host.rest.loadTracks(query);
|
|
334
|
+
if (result.error || result.tracks.length === 0) {
|
|
335
|
+
throw new LoadError(result.error ?? `no tracks for ${query}`);
|
|
336
|
+
}
|
|
337
|
+
return result.loadType === "playlist" /* Playlist */ ? result.tracks : [result.tracks[0]];
|
|
338
|
+
}
|
|
339
|
+
async nextTrack(forced) {
|
|
340
|
+
if (this.loop === "track" /* Track */ && this.current !== null && !forced) {
|
|
341
|
+
return this.current;
|
|
342
|
+
}
|
|
343
|
+
if (this.queue.length > 0) {
|
|
344
|
+
if (this.current !== null) this.pushHistory(this.current);
|
|
345
|
+
return this.queue.shift();
|
|
346
|
+
}
|
|
347
|
+
if (this.loop === "queue" /* Queue */ && this.history.length > 0) {
|
|
348
|
+
this.queue = [...this.history, ...this.current ? [this.current] : []];
|
|
349
|
+
this.history = [];
|
|
350
|
+
return this.queue.shift() ?? null;
|
|
351
|
+
}
|
|
352
|
+
if (this.autoplay !== null) return this.autoplay(this);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
async advance(forced = false) {
|
|
356
|
+
const next = await this.nextTrack(forced);
|
|
357
|
+
if (next === null) {
|
|
358
|
+
this.current = null;
|
|
359
|
+
if (this.created) await this.host.rest.stop(this.guildId);
|
|
360
|
+
this.host.emit("queueEnd", this);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
await this.playTrack(next);
|
|
364
|
+
}
|
|
365
|
+
async playTrack(track) {
|
|
366
|
+
if (!this.connected) throw new NotConnected(`guild ${this.guildId} has no voice connection`);
|
|
367
|
+
this.current = track;
|
|
368
|
+
if (!this.created) {
|
|
369
|
+
await this.host.rest.createPlayer(this.guildId, this.voicePayload(), {
|
|
370
|
+
track: track.encoded,
|
|
371
|
+
volume: this.volume,
|
|
372
|
+
paused: this.paused
|
|
373
|
+
});
|
|
374
|
+
this.created = true;
|
|
375
|
+
} else {
|
|
376
|
+
await this.host.rest.play(this.guildId, track.encoded);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async setPausedInternal(paused) {
|
|
380
|
+
this.paused = paused;
|
|
381
|
+
if (this.created) await this.host.rest.setPaused(this.guildId, paused);
|
|
382
|
+
}
|
|
383
|
+
/** Called by the client's WS loop when a TRACK_END for this guild arrives. */
|
|
384
|
+
async onTrackEnd(advances) {
|
|
385
|
+
if (advances) await this.advance();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/rest.ts
|
|
390
|
+
var REST = class {
|
|
391
|
+
base;
|
|
392
|
+
headers;
|
|
393
|
+
constructor(baseUrl, token) {
|
|
394
|
+
this.base = baseUrl.replace(/\/$/, "");
|
|
395
|
+
this.headers = {
|
|
396
|
+
Authorization: `Bearer ${token}`,
|
|
397
|
+
"Content-Type": "application/json"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async request(method, path, body) {
|
|
401
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
402
|
+
method,
|
|
403
|
+
headers: this.headers,
|
|
404
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
405
|
+
});
|
|
406
|
+
if (res.status === 401 || res.status === 403) throw new AuthError(await res.text());
|
|
407
|
+
if (res.status === 429) {
|
|
408
|
+
const retry = res.headers.get("Retry-After");
|
|
409
|
+
throw new RateLimited(await res.text(), retry ? Number(retry) : void 0);
|
|
410
|
+
}
|
|
411
|
+
if (res.status >= 400) throw new APIError(res.status, await res.text());
|
|
412
|
+
const ct = res.headers.get("Content-Type") ?? "";
|
|
413
|
+
return ct.includes("application/json") ? res.json() : res.text();
|
|
414
|
+
}
|
|
415
|
+
// --- metadata ---------------------------------------------------------
|
|
416
|
+
async loadTracks(identifier) {
|
|
417
|
+
const q = new URLSearchParams({ identifier });
|
|
418
|
+
return loadResultFromJson(await this.request("GET", `/v1/loadtracks?${q}`));
|
|
419
|
+
}
|
|
420
|
+
async prewarm(encoded, guildId) {
|
|
421
|
+
const body = { tracks: encoded.map((e) => ({ encoded: e })) };
|
|
422
|
+
if (guildId !== void 0) body.guild_id = guildId;
|
|
423
|
+
return this.request("POST", "/v1/tracks/prewarm", body);
|
|
424
|
+
}
|
|
425
|
+
// --- player lifecycle -------------------------------------------------
|
|
426
|
+
async createPlayer(guildId, voice, opts = {}) {
|
|
427
|
+
const body = {
|
|
428
|
+
voice,
|
|
429
|
+
volume: opts.volume ?? 100,
|
|
430
|
+
paused: opts.paused ?? false
|
|
431
|
+
};
|
|
432
|
+
if (opts.track) body.track = { encoded: opts.track };
|
|
433
|
+
if (opts.audioGraph !== void 0) body.filters = { audio_graph: opts.audioGraph };
|
|
434
|
+
return this.request("POST", `/v1/guilds/${guildId}/players`, body);
|
|
435
|
+
}
|
|
436
|
+
async destroyPlayer(guildId) {
|
|
437
|
+
return this.request("DELETE", `/v1/guilds/${guildId}/players`);
|
|
438
|
+
}
|
|
439
|
+
async getState(guildId) {
|
|
440
|
+
return playerStateFromJson(await this.request("GET", `/v1/guilds/${guildId}/player`));
|
|
441
|
+
}
|
|
442
|
+
// --- playback control -------------------------------------------------
|
|
443
|
+
async play(guildId, track, audioGraph) {
|
|
444
|
+
const body = { track: { encoded: track } };
|
|
445
|
+
if (audioGraph !== void 0) body.filters = { audio_graph: audioGraph };
|
|
446
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/play`, body);
|
|
447
|
+
}
|
|
448
|
+
async stop(guildId) {
|
|
449
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/stop`, {});
|
|
450
|
+
}
|
|
451
|
+
async setPaused(guildId, paused) {
|
|
452
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/pause`, { paused });
|
|
453
|
+
}
|
|
454
|
+
async setVolume(guildId, volume) {
|
|
455
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/volume`, { volume });
|
|
456
|
+
}
|
|
457
|
+
async seek(guildId, positionMs) {
|
|
458
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/seek`, {
|
|
459
|
+
position_ms: positionMs
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async setFilters(guildId, audioGraph) {
|
|
463
|
+
return this.request("POST", `/v1/guilds/${guildId}/player/filters`, {
|
|
464
|
+
filters: { audio_graph: audioGraph }
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/client.ts
|
|
470
|
+
var EVENT_OPS = {
|
|
471
|
+
TRACK_START: "trackStart",
|
|
472
|
+
TRACK_END: "trackEnd",
|
|
473
|
+
TRACK_EXCEPTION: "trackException",
|
|
474
|
+
TRACK_STUCK: "trackStuck",
|
|
475
|
+
PLAYER_UPDATE: "playerUpdate",
|
|
476
|
+
VOICE_CLOSED: "voiceClosed",
|
|
477
|
+
STATS: "stats"
|
|
478
|
+
};
|
|
479
|
+
var Client = class extends import_node_events.EventEmitter {
|
|
480
|
+
rest;
|
|
481
|
+
baseUrl;
|
|
482
|
+
token;
|
|
483
|
+
reconnect;
|
|
484
|
+
playersMap = /* @__PURE__ */ new Map();
|
|
485
|
+
ws = null;
|
|
486
|
+
heartbeatTimer = null;
|
|
487
|
+
reconnectTimer = null;
|
|
488
|
+
sessionId = null;
|
|
489
|
+
closed = false;
|
|
490
|
+
backoff = 1e3;
|
|
491
|
+
hasConnected = false;
|
|
492
|
+
heartbeatSentAt = 0;
|
|
493
|
+
/** Last measured heartbeat round-trip latency in ms (-1 until first ack). */
|
|
494
|
+
latencyMs = -1;
|
|
495
|
+
/** Most recent STATS frame payload from the node, or null. */
|
|
496
|
+
lastStats = null;
|
|
497
|
+
constructor(baseUrl, token, opts = {}) {
|
|
498
|
+
super();
|
|
499
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
500
|
+
this.token = token;
|
|
501
|
+
this.reconnect = opts.reconnect ?? true;
|
|
502
|
+
this.rest = new REST(this.baseUrl, token);
|
|
503
|
+
}
|
|
504
|
+
// --- lifecycle --------------------------------------------------------
|
|
505
|
+
async start() {
|
|
506
|
+
this.closed = false;
|
|
507
|
+
this.openSocket();
|
|
508
|
+
}
|
|
509
|
+
async close() {
|
|
510
|
+
this.closed = true;
|
|
511
|
+
if (this.heartbeatTimer) {
|
|
512
|
+
clearInterval(this.heartbeatTimer);
|
|
513
|
+
this.heartbeatTimer = null;
|
|
514
|
+
}
|
|
515
|
+
if (this.reconnectTimer) {
|
|
516
|
+
clearTimeout(this.reconnectTimer);
|
|
517
|
+
this.reconnectTimer = null;
|
|
518
|
+
}
|
|
519
|
+
this.ws?.removeAllListeners();
|
|
520
|
+
this.ws?.close();
|
|
521
|
+
this.ws = null;
|
|
522
|
+
}
|
|
523
|
+
/** Emit "error" without the Node EventEmitter footgun: an unlistened "error"
|
|
524
|
+
* throws and would crash the host process, so fall back to a console warning. */
|
|
525
|
+
safeError(err) {
|
|
526
|
+
if (this.listenerCount("error") > 0) this.emit("error", err);
|
|
527
|
+
else console.warn("[auralis] unhandled client error:", err);
|
|
528
|
+
}
|
|
529
|
+
// --- players ----------------------------------------------------------
|
|
530
|
+
getPlayer(guildId) {
|
|
531
|
+
let p = this.playersMap.get(guildId);
|
|
532
|
+
if (!p) {
|
|
533
|
+
p = new Player(this, guildId);
|
|
534
|
+
this.playersMap.set(guildId, p);
|
|
535
|
+
}
|
|
536
|
+
return p;
|
|
537
|
+
}
|
|
538
|
+
players() {
|
|
539
|
+
return [...this.playersMap.values()];
|
|
540
|
+
}
|
|
541
|
+
dropPlayer(guildId) {
|
|
542
|
+
this.playersMap.delete(guildId);
|
|
543
|
+
}
|
|
544
|
+
// --- metadata convenience --------------------------------------------
|
|
545
|
+
loadTracks(identifier) {
|
|
546
|
+
return this.rest.loadTracks(identifier);
|
|
547
|
+
}
|
|
548
|
+
/** `ytsearch:`-prefix a bare query and return the hits (empty on miss). */
|
|
549
|
+
async search(query) {
|
|
550
|
+
const first = query.split(" ", 1)[0];
|
|
551
|
+
const ident = first.includes(":") ? query : `ytsearch:${query}`;
|
|
552
|
+
return (await this.loadTracks(ident)).tracks;
|
|
553
|
+
}
|
|
554
|
+
// --- websocket --------------------------------------------------------
|
|
555
|
+
get wsUrl() {
|
|
556
|
+
return this.baseUrl.replace(/^http/, "ws") + "/v1/ws";
|
|
557
|
+
}
|
|
558
|
+
openSocket() {
|
|
559
|
+
if (this.closed) return;
|
|
560
|
+
const ws = new import_ws.default(this.wsUrl, {
|
|
561
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
562
|
+
});
|
|
563
|
+
this.ws = ws;
|
|
564
|
+
ws.on("open", () => {
|
|
565
|
+
this.backoff = 1e3;
|
|
566
|
+
this.subscribeAll();
|
|
567
|
+
});
|
|
568
|
+
ws.on("message", (raw) => {
|
|
569
|
+
let frame;
|
|
570
|
+
try {
|
|
571
|
+
frame = JSON.parse(raw.toString());
|
|
572
|
+
} catch {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.handleFrame(frame).catch((e) => this.safeError(e));
|
|
576
|
+
});
|
|
577
|
+
ws.on("error", (err) => this.safeError(err));
|
|
578
|
+
ws.on("close", () => {
|
|
579
|
+
if (this.heartbeatTimer) {
|
|
580
|
+
clearInterval(this.heartbeatTimer);
|
|
581
|
+
this.heartbeatTimer = null;
|
|
582
|
+
}
|
|
583
|
+
this.ws = null;
|
|
584
|
+
if (this.closed || !this.reconnect) return;
|
|
585
|
+
this.emit("disconnect");
|
|
586
|
+
const jitter = this.backoff * (0.75 + Math.random() * 0.5);
|
|
587
|
+
this.reconnectTimer = setTimeout(() => this.openSocket(), jitter);
|
|
588
|
+
this.backoff = Math.min(this.backoff * 2, 3e4);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/** After a reconnect, pull each player's authoritative state from the node so
|
|
592
|
+
* position/volume/paused are accurate again. Per-player failures are ignored. */
|
|
593
|
+
async resyncPlayers() {
|
|
594
|
+
await Promise.all(
|
|
595
|
+
this.players().map(async (p) => {
|
|
596
|
+
try {
|
|
597
|
+
p.state = await this.rest.getState(p.guildId);
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
subscribeAll() {
|
|
604
|
+
if (this.ws?.readyState === import_ws.default.OPEN && this.playersMap.size > 0) {
|
|
605
|
+
this.send({
|
|
606
|
+
op: "SUBSCRIBE",
|
|
607
|
+
d: { guilds: [...this.playersMap.keys()].map(String) }
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
send(payload) {
|
|
612
|
+
if (this.ws?.readyState === import_ws.default.OPEN) this.ws.send(JSON.stringify(payload));
|
|
613
|
+
}
|
|
614
|
+
async handleFrame(frame) {
|
|
615
|
+
const op = frame.op;
|
|
616
|
+
const d = frame.d ?? {};
|
|
617
|
+
if (op === "HELLO") {
|
|
618
|
+
const interval = d.heartbeat_interval_ms ?? 3e4;
|
|
619
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
620
|
+
let seq = 0;
|
|
621
|
+
this.heartbeatTimer = setInterval(() => {
|
|
622
|
+
this.heartbeatSentAt = Date.now();
|
|
623
|
+
this.send({ op: "HEARTBEAT", d: { seq: ++seq } });
|
|
624
|
+
}, interval);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (op === "READY") {
|
|
628
|
+
const resumed = this.hasConnected;
|
|
629
|
+
this.sessionId = d.session_id ?? null;
|
|
630
|
+
this.hasConnected = true;
|
|
631
|
+
this.subscribeAll();
|
|
632
|
+
if (resumed) {
|
|
633
|
+
await this.resyncPlayers();
|
|
634
|
+
this.emit("resumed");
|
|
635
|
+
} else {
|
|
636
|
+
this.emit("ready");
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (op === "HEARTBEAT_ACK") {
|
|
641
|
+
if (this.heartbeatSentAt) this.latencyMs = Date.now() - this.heartbeatSentAt;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (op === "STATS") {
|
|
645
|
+
this.lastStats = d;
|
|
646
|
+
}
|
|
647
|
+
const guildId = d.guild_id ? Number(d.guild_id) : null;
|
|
648
|
+
const player = guildId !== null ? this.playersMap.get(guildId) : void 0;
|
|
649
|
+
if (op === "TRACK_END" && player) {
|
|
650
|
+
await player.onTrackEnd(endReasonAdvances(d.reason ?? "FINISHED"));
|
|
651
|
+
} else if (op === "PLAYER_UPDATE" && player) {
|
|
652
|
+
player.state = playerStateFromJson(d.state ?? {});
|
|
653
|
+
}
|
|
654
|
+
const eventName = EVENT_OPS[op];
|
|
655
|
+
if (eventName) this.emit(eventName, player ?? guildId, d);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
659
|
+
0 && (module.exports = {
|
|
660
|
+
APIError,
|
|
661
|
+
AuralisError,
|
|
662
|
+
AuthError,
|
|
663
|
+
Client,
|
|
664
|
+
EndReason,
|
|
665
|
+
LoadError,
|
|
666
|
+
LoadType,
|
|
667
|
+
LoopMode,
|
|
668
|
+
NotConnected,
|
|
669
|
+
Player,
|
|
670
|
+
REST,
|
|
671
|
+
RateLimited,
|
|
672
|
+
endReasonAdvances
|
|
673
|
+
});
|
|
674
|
+
//# sourceMappingURL=index.cjs.map
|