auralis-sdk 0.4.3 → 0.5.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.cjs +211 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -7
- package/dist/index.d.ts +87 -7
- package/dist/index.js +211 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -75,12 +75,12 @@ var LoadError = class extends AuralisError {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// src/types.ts
|
|
78
|
-
var BIG_TAG = "
|
|
78
|
+
var BIG_TAG = "||BIGINT||";
|
|
79
79
|
function snowflakeStringify(obj) {
|
|
80
80
|
return JSON.stringify(
|
|
81
81
|
obj,
|
|
82
82
|
(_k, v) => typeof v === "bigint" ? `${BIG_TAG}${v}${BIG_TAG}` : v
|
|
83
|
-
).replace(
|
|
83
|
+
).replace(/"\|\|BIGINT\|\|(\d+)\|\|BIGINT\|\|"/g, "$1");
|
|
84
84
|
}
|
|
85
85
|
var LoopMode = /* @__PURE__ */ ((LoopMode2) => {
|
|
86
86
|
LoopMode2["Off"] = "off";
|
|
@@ -189,6 +189,9 @@ var Player = class {
|
|
|
189
189
|
maxPositionMs = 0;
|
|
190
190
|
/** Alternate-upload attempts spent on the current dead track. */
|
|
191
191
|
recoverAttempts = 0;
|
|
192
|
+
/** Wall-clock time (ms) when the last PLAYER_UPDATE position was received.
|
|
193
|
+
* Used by the {@link positionMs} interpolating getter. */
|
|
194
|
+
_positionTimestamp = 0;
|
|
192
195
|
// --- voice plumbing ---------------------------------------------------
|
|
193
196
|
handleVoiceServerUpdate(data) {
|
|
194
197
|
this.voiceServer = { token: data.token, endpoint: data.endpoint };
|
|
@@ -214,6 +217,7 @@ var Player = class {
|
|
|
214
217
|
* spotted, and clears the recovery counter once a track is genuinely playing. */
|
|
215
218
|
applyState(state) {
|
|
216
219
|
this.state = state;
|
|
220
|
+
this._positionTimestamp = Date.now();
|
|
217
221
|
if (state.positionMs > this.maxPositionMs) this.maxPositionMs = state.positionMs;
|
|
218
222
|
if (this.maxPositionMs > this.deadStreamThresholdMs) this.recoverAttempts = 0;
|
|
219
223
|
}
|
|
@@ -362,6 +366,7 @@ var Player = class {
|
|
|
362
366
|
const prev = this.history.pop();
|
|
363
367
|
if (!prev) return;
|
|
364
368
|
if (this.current !== null) this.queue.unshift(this.current);
|
|
369
|
+
this.current = null;
|
|
365
370
|
this.queue.unshift(prev);
|
|
366
371
|
await this.advance(true);
|
|
367
372
|
}
|
|
@@ -369,17 +374,139 @@ var Player = class {
|
|
|
369
374
|
async setBassBoost(gainDb) {
|
|
370
375
|
await this.setFilters(gainDb === 0 ? null : `bass=g=${gainDb}`);
|
|
371
376
|
}
|
|
372
|
-
/** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal.
|
|
377
|
+
/** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal.
|
|
378
|
+
* Uses `atempo` for tempo and `asetrate` for pitch shift — both evaluated
|
|
379
|
+
* in JS so the filter string contains literal numbers, not expressions. */
|
|
373
380
|
async setSpeed(rate) {
|
|
374
381
|
const r = Math.max(0.5, Math.min(2, rate));
|
|
375
|
-
await this.setFilters(r === 1 ? null : `atempo=${r},asetrate
|
|
382
|
+
await this.setFilters(r === 1 ? null : `atempo=${r},asetrate=${48e3 * r}`);
|
|
376
383
|
}
|
|
377
384
|
nowPlaying() {
|
|
378
385
|
return this.current;
|
|
379
386
|
}
|
|
387
|
+
/** Enqueue a track or query to play immediately after the current one.
|
|
388
|
+
* Unlike `insert(0, ...)`, this is an explicit "play next" intent — it
|
|
389
|
+
* always inserts at position 0 regardless of queue state and starts
|
|
390
|
+
* playback if the player is idle. */
|
|
391
|
+
async playNext(query, requester) {
|
|
392
|
+
return this.insert(0, query, requester);
|
|
393
|
+
}
|
|
394
|
+
/** Remove all duplicate tracks from the queue (keeps first occurrence).
|
|
395
|
+
* Uniqueness is determined by `encoded` token. Returns the number removed. */
|
|
396
|
+
dedupe() {
|
|
397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
398
|
+
const before = this.queue.length;
|
|
399
|
+
this.queue = this.queue.filter((t) => {
|
|
400
|
+
if (seen.has(t.encoded)) return false;
|
|
401
|
+
seen.add(t.encoded);
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
return before - this.queue.length;
|
|
405
|
+
}
|
|
406
|
+
/** Find all tracks in the queue whose title or author contains `query`
|
|
407
|
+
* (case-insensitive). Returns `[index, track]` pairs so callers can remove
|
|
408
|
+
* or jump to a result by index. */
|
|
409
|
+
find(query) {
|
|
410
|
+
const q = query.toLowerCase();
|
|
411
|
+
return this.queue.reduce((acc, t, i) => {
|
|
412
|
+
if (t.title.toLowerCase().includes(q) || t.author.toLowerCase().includes(q)) {
|
|
413
|
+
acc.push([i, t]);
|
|
414
|
+
}
|
|
415
|
+
return acc;
|
|
416
|
+
}, []);
|
|
417
|
+
}
|
|
418
|
+
/** Total duration of all queued tracks in milliseconds. Streams count as 0.
|
|
419
|
+
* Does not include the currently playing track. */
|
|
420
|
+
get totalDurationMs() {
|
|
421
|
+
return this.queue.reduce((sum, t) => sum + (t.isStream ? 0 : t.lengthMs), 0);
|
|
422
|
+
}
|
|
423
|
+
/** Interpolated playback position in milliseconds. Between `PLAYER_UPDATE`
|
|
424
|
+
* frames this advances using wall-clock time so progress bars stay smooth.
|
|
425
|
+
* Returns 0 if nothing is playing or the player is paused. */
|
|
426
|
+
get positionMs() {
|
|
427
|
+
if (!this.state.playing || this.state.paused) return this.state.positionMs;
|
|
428
|
+
const elapsed = Date.now() - this._positionTimestamp;
|
|
429
|
+
return Math.min(
|
|
430
|
+
this.state.positionMs + elapsed,
|
|
431
|
+
this.current?.lengthMs ?? this.state.positionMs + elapsed
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
/** Serialize current queue, history, loop mode and volume into a plain object
|
|
435
|
+
* that can be JSON-stringified and stored. Restore with {@link Player.restore}. */
|
|
436
|
+
export() {
|
|
437
|
+
return {
|
|
438
|
+
version: 1,
|
|
439
|
+
guildId: String(this.guildId),
|
|
440
|
+
queue: this.queue.map((t) => ({ ...t })),
|
|
441
|
+
history: this.history.map((t) => ({ ...t })),
|
|
442
|
+
current: this.current ? { ...this.current } : null,
|
|
443
|
+
loop: this.loop,
|
|
444
|
+
volume: this.volume,
|
|
445
|
+
paused: this.paused
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/** Restore queue/history/loop/volume from a previously exported snapshot.
|
|
449
|
+
* Does NOT auto-start playback — call `startIfIdle()` or let the next
|
|
450
|
+
* voice event trigger it. */
|
|
451
|
+
restore(snapshot) {
|
|
452
|
+
this.queue = snapshot.queue.map((t) => ({ ...t }));
|
|
453
|
+
this.history = snapshot.history.map((t) => ({ ...t }));
|
|
454
|
+
this.current = snapshot.current ? { ...snapshot.current } : null;
|
|
455
|
+
this.loop = snapshot.loop;
|
|
456
|
+
this.volume = snapshot.volume;
|
|
457
|
+
this.paused = snapshot.paused;
|
|
458
|
+
}
|
|
459
|
+
/** Apply a named EQ preset. `"flat"` clears all filters.
|
|
460
|
+
*
|
|
461
|
+
* | preset | effect |
|
|
462
|
+
* |-------------|---------------------------------------------|
|
|
463
|
+
* | flat | no filters (passthrough) |
|
|
464
|
+
* | bassboost | heavy bass boost (+8 dB) |
|
|
465
|
+
* | soft | gentle bass lift (+3 dB) |
|
|
466
|
+
* | nightcore | speed+pitch up (1.3×) |
|
|
467
|
+
* | vaporwave | speed+pitch down (0.8×) |
|
|
468
|
+
* | earrape | extreme distortion / clipping (use wisely) |
|
|
469
|
+
* | treble | high-frequency boost (+6 dB) |
|
|
470
|
+
* | karaoke | basic vocal removal |
|
|
471
|
+
*/
|
|
472
|
+
async setEqualizer(preset) {
|
|
473
|
+
const graphs = {
|
|
474
|
+
flat: null,
|
|
475
|
+
bassboost: "bass=g=8",
|
|
476
|
+
soft: "bass=g=3",
|
|
477
|
+
nightcore: `atempo=1.3,asetrate=${Math.round(48e3 * 1.3)}`,
|
|
478
|
+
vaporwave: `atempo=0.8,asetrate=${Math.round(48e3 * 0.8)}`,
|
|
479
|
+
earrape: "acrusher=level_in=8:level_out=18:bits=8:mode=log:aa=1",
|
|
480
|
+
treble: "treble=g=6",
|
|
481
|
+
karaoke: "pan=stereo|c0=c0-c1|c1=c1-c0"
|
|
482
|
+
};
|
|
483
|
+
await this.setFilters(graphs[preset]);
|
|
484
|
+
}
|
|
485
|
+
/** Reverse the current queue order in-place. */
|
|
486
|
+
reverse() {
|
|
487
|
+
this.queue.reverse();
|
|
488
|
+
}
|
|
489
|
+
/** Remove all tracks from the queue that were requested by `requesterId`. */
|
|
490
|
+
removeBy(requesterId) {
|
|
491
|
+
const id = String(requesterId);
|
|
492
|
+
const before = this.queue.length;
|
|
493
|
+
this.queue = this.queue.filter((t) => String(t.extras.requester) !== id);
|
|
494
|
+
return before - this.queue.length;
|
|
495
|
+
}
|
|
496
|
+
/** Peek at the next N tracks without mutating state. Defaults to 5. */
|
|
497
|
+
peek(n = 5) {
|
|
498
|
+
return this.queue.slice(0, n);
|
|
499
|
+
}
|
|
500
|
+
/** Returns true if nothing is queued and nothing is playing. */
|
|
501
|
+
get idle() {
|
|
502
|
+
return this.current === null && this.queue.length === 0;
|
|
503
|
+
}
|
|
380
504
|
async disconnect() {
|
|
381
505
|
this.queue = [];
|
|
382
506
|
this.current = null;
|
|
507
|
+
this.history = [];
|
|
508
|
+
this.paused = false;
|
|
509
|
+
this.volume = 100;
|
|
383
510
|
if (this.created) await this.host.rest.destroyPlayer(this.guildId);
|
|
384
511
|
this.created = false;
|
|
385
512
|
this.voiceServer = null;
|
|
@@ -410,10 +537,13 @@ var Player = class {
|
|
|
410
537
|
if (this.current !== null) this.pushHistory(this.current);
|
|
411
538
|
return this.queue.shift();
|
|
412
539
|
}
|
|
413
|
-
if (this.loop === "queue" /* Queue */
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
540
|
+
if (this.loop === "queue" /* Queue */) {
|
|
541
|
+
const cycle = [...this.history, ...this.current ? [this.current] : []];
|
|
542
|
+
if (cycle.length > 0) {
|
|
543
|
+
this.queue = cycle;
|
|
544
|
+
this.history = [];
|
|
545
|
+
return this.queue.shift() ?? null;
|
|
546
|
+
}
|
|
417
547
|
}
|
|
418
548
|
if (this.autoplay !== null) return this.autoplay(this);
|
|
419
549
|
return null;
|
|
@@ -425,14 +555,27 @@ var Player = class {
|
|
|
425
555
|
return this.serialize(() => this.advanceInner(forced));
|
|
426
556
|
}
|
|
427
557
|
async advanceInner(forced = false) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
558
|
+
for (let attempts = this.queue.length + 1; attempts >= 0; attempts--) {
|
|
559
|
+
const next = await this.nextTrack(forced);
|
|
560
|
+
if (next === null) {
|
|
561
|
+
this.current = null;
|
|
562
|
+
if (this.created) await this.host.rest.stop(this.guildId);
|
|
563
|
+
this.host.emit("queueEnd", this);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
await this.playTrack(next);
|
|
568
|
+
return;
|
|
569
|
+
} catch (e) {
|
|
570
|
+
if (e instanceof NotConnected) {
|
|
571
|
+
this.queue.unshift(next);
|
|
572
|
+
throw e;
|
|
573
|
+
}
|
|
574
|
+
this.current = null;
|
|
575
|
+
forced = true;
|
|
576
|
+
this.host.emit("trackException", this, { track: next, error: String(e) });
|
|
577
|
+
}
|
|
434
578
|
}
|
|
435
|
-
await this.playTrack(next);
|
|
436
579
|
}
|
|
437
580
|
async playTrack(track) {
|
|
438
581
|
if (!this.connected) throw new NotConnected(`guild ${this.guildId} has no voice connection`);
|
|
@@ -678,6 +821,38 @@ var Client = class extends import_node_events.EventEmitter {
|
|
|
678
821
|
this.closed = false;
|
|
679
822
|
this.openSocket();
|
|
680
823
|
}
|
|
824
|
+
/** Resolves once the client fires `ready` or `resumed`. If it is already
|
|
825
|
+
* connected it resolves on the next tick. Rejects after `timeoutMs` (default
|
|
826
|
+
* 30 s) so callers don't hang forever on a bad URL/token. */
|
|
827
|
+
waitForReady(timeoutMs = 3e4) {
|
|
828
|
+
return new Promise((resolve, reject) => {
|
|
829
|
+
if (this.hasConnected) {
|
|
830
|
+
resolve();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const onReady = () => {
|
|
834
|
+
clearTimeout(timer);
|
|
835
|
+
resolve();
|
|
836
|
+
};
|
|
837
|
+
const onResumed = () => {
|
|
838
|
+
clearTimeout(timer);
|
|
839
|
+
resolve();
|
|
840
|
+
};
|
|
841
|
+
const timer = setTimeout(() => {
|
|
842
|
+
this.off("ready", onReady);
|
|
843
|
+
this.off("resumed", onResumed);
|
|
844
|
+
reject(new Error("[auralis] waitForReady timed out"));
|
|
845
|
+
}, timeoutMs);
|
|
846
|
+
this.once("ready", onReady);
|
|
847
|
+
this.once("resumed", onResumed);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
/** Gracefully disconnect every active player then close the WebSocket.
|
|
851
|
+
* Prefer this over `close()` when you want clean backend teardown. */
|
|
852
|
+
async destroy() {
|
|
853
|
+
await Promise.allSettled(this.players().map((p) => p.disconnect()));
|
|
854
|
+
await this.close();
|
|
855
|
+
}
|
|
681
856
|
async close() {
|
|
682
857
|
this.closed = true;
|
|
683
858
|
if (this.heartbeatTimer) {
|
|
@@ -705,6 +880,8 @@ var Client = class extends import_node_events.EventEmitter {
|
|
|
705
880
|
if (!p) {
|
|
706
881
|
p = new Player(this, id);
|
|
707
882
|
this.playersMap.set(id, p);
|
|
883
|
+
this.subscribeGuild(id);
|
|
884
|
+
this.emit("playerCreated", p);
|
|
708
885
|
}
|
|
709
886
|
return p;
|
|
710
887
|
}
|
|
@@ -713,16 +890,22 @@ var Client = class extends import_node_events.EventEmitter {
|
|
|
713
890
|
}
|
|
714
891
|
dropPlayer(guildId) {
|
|
715
892
|
this.playersMap.delete(guildId);
|
|
893
|
+
this.emit("playerDestroyed", guildId);
|
|
716
894
|
}
|
|
717
895
|
// --- metadata convenience --------------------------------------------
|
|
718
896
|
loadTracks(identifier) {
|
|
719
897
|
return this.rest.loadTracks(identifier);
|
|
720
898
|
}
|
|
721
|
-
/** `ytsearch:`-prefix a bare query and return the hits
|
|
899
|
+
/** `ytsearch:`-prefix a bare query and return the hits. Throws `LoadError` on
|
|
900
|
+
* a server-side error so callers aren't silently handed an empty array. */
|
|
722
901
|
async search(query) {
|
|
723
902
|
const first = query.split(" ", 1)[0];
|
|
724
903
|
const ident = first.includes(":") ? query : `ytsearch:${query}`;
|
|
725
|
-
|
|
904
|
+
const result = await this.loadTracks(ident);
|
|
905
|
+
if (result.loadType === "error" /* Error */) {
|
|
906
|
+
throw new LoadError(result.error ?? `search failed for: ${query}`);
|
|
907
|
+
}
|
|
908
|
+
return result.tracks;
|
|
726
909
|
}
|
|
727
910
|
// --- websocket --------------------------------------------------------
|
|
728
911
|
get wsUrl() {
|
|
@@ -781,6 +964,14 @@ var Client = class extends import_node_events.EventEmitter {
|
|
|
781
964
|
});
|
|
782
965
|
}
|
|
783
966
|
}
|
|
967
|
+
subscribeGuild(guildId) {
|
|
968
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
969
|
+
this.send({
|
|
970
|
+
op: "SUBSCRIBE",
|
|
971
|
+
d: { guilds: [String(guildId)] }
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
784
975
|
send(payload) {
|
|
785
976
|
if (this.ws?.readyState === import_ws.default.OPEN) this.ws.send(JSON.stringify(payload));
|
|
786
977
|
}
|
|
@@ -816,6 +1007,9 @@ var Client = class extends import_node_events.EventEmitter {
|
|
|
816
1007
|
}
|
|
817
1008
|
if (op === "STATS") {
|
|
818
1009
|
this.lastStats = d;
|
|
1010
|
+
const eventName2 = EVENT_OPS[op];
|
|
1011
|
+
if (eventName2) this.emit(eventName2, null, d);
|
|
1012
|
+
return;
|
|
819
1013
|
}
|
|
820
1014
|
const guildId = d.guild_id != null ? BigInt(d.guild_id) : null;
|
|
821
1015
|
const player = guildId !== null ? this.playersMap.get(guildId) : void 0;
|