linkdave 0.1.6 → 0.2.0-dev.67c58c7

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/client.d.ts CHANGED
@@ -24,7 +24,10 @@ export declare class LinkDaveClient extends EventEmitter {
24
24
  connectAll(): Promise<void>;
25
25
  disconnectAll(): void;
26
26
  getBestNode(): Node | undefined;
27
- getPlayer(guildId: string, options?: Omit<PlayerOptions, "guildId">): Player;
27
+ private getPreferredNode;
28
+ getPlayer(guildId: string, options?: Omit<PlayerOptions, "guildId"> & {
29
+ nodeId?: string;
30
+ }): Player;
28
31
  removePlayer(guildId: string): boolean;
29
32
  get players(): ReadonlyMap<string, Player>;
30
33
  get clientId(): string;
@@ -33,4 +36,3 @@ export declare class LinkDaveClient extends EventEmitter {
33
36
  _onPlayerDestroy(guildId: string): void;
34
37
  _updatePlayerNode(guildId: string, oldNode: Node, newNode: Node): void;
35
38
  }
36
- //# sourceMappingURL=client.d.ts.map
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ import { GatewayDispatchEvents } from "discord-api-types/v10";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { Node } from "./node.js";
4
4
  import { Player } from "./player.js";
5
- import { EventName, ManagerEventName } from "./types.js";
5
+ import { DisconnectReason, EventName, ManagerEventName } from "./types.js";
6
6
  const CLIENT_ID_REGEX = /^\d{15,21}$/;
7
7
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
8
8
  export class LinkDaveClient extends EventEmitter {
@@ -70,13 +70,25 @@ export class LinkDaveClient extends EventEmitter {
70
70
  }
71
71
  return bestNode;
72
72
  }
73
+ getPreferredNode(nodeId) {
74
+ if (!nodeId) {
75
+ return this.getBestNode();
76
+ }
77
+ const node = this.#nodes.get(nodeId);
78
+ if (!node || !node.connected || node.draining) {
79
+ return undefined;
80
+ }
81
+ return node;
82
+ }
73
83
  getPlayer(guildId, options) {
74
84
  let player = this.#players.get(guildId);
75
85
  if (player)
76
86
  return player;
77
- const node = this.getBestNode();
87
+ const node = this.getPreferredNode(options?.nodeId);
78
88
  if (!node) {
79
- throw new Error("No available nodes to create player");
89
+ throw new Error(options?.nodeId
90
+ ? `Node "${options.nodeId}" is not available to create player`
91
+ : "No available nodes to create player");
80
92
  }
81
93
  player = new Player(this, guildId, node, options);
82
94
  this.#players.set(guildId, player);
@@ -104,7 +116,6 @@ export class LinkDaveClient extends EventEmitter {
104
116
  handleRaw({ t: event, d: data }) {
105
117
  switch (event) {
106
118
  case GatewayDispatchEvents.VoiceStateUpdate: {
107
- // https://discord.com/developers/docs/resources/voice#voice-state-object
108
119
  if (!data.guild_id)
109
120
  return;
110
121
  if (data.user_id !== this.#clientId)
@@ -134,9 +145,8 @@ export class LinkDaveClient extends EventEmitter {
134
145
  node.on(EventName.TrackEnd, (data) => this.#handleTrackEnd(node, data));
135
146
  node.on(EventName.TrackError, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.TrackError, data));
136
147
  node.on(EventName.QueueError, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.QueueError, data));
137
- node.on(EventName.VoiceConnect, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.VoiceConnect, data));
148
+ node.on(EventName.VoiceConnect, (data) => this.#handleVoiceConnect(node, data));
138
149
  node.on(EventName.VoiceDisconnect, (data) => this.#handleVoiceDisconnect(node, data));
139
- node.on(EventName.Pong, () => this.emit(EventName.Pong, undefined));
140
150
  node.on(EventName.Stats, (data) => this.emit(EventName.Stats, data));
141
151
  node.on(EventName.NodeDraining, (data) => this.#handleNodeDraining(node, data));
142
152
  node.on(EventName.MigrateReady, (data) => this.#handleMigrateReady(node, data));
@@ -153,7 +163,7 @@ export class LinkDaveClient extends EventEmitter {
153
163
  const player = this.#players.get(data.guild_id);
154
164
  if (player?.node !== node)
155
165
  return;
156
- player._updateState(data);
166
+ player._onPlayerUpdate(data);
157
167
  this.emit(EventName.PlayerUpdate, data);
158
168
  }
159
169
  #handleTrackStart(node, data) {
@@ -170,14 +180,37 @@ export class LinkDaveClient extends EventEmitter {
170
180
  player._onTrackEnd(data);
171
181
  this.emit(EventName.TrackEnd, data);
172
182
  }
183
+ #handleVoiceConnect(node, data) {
184
+ const player = this.#players.get(data.guild_id);
185
+ if (player?.node !== node)
186
+ return;
187
+ player._onVoiceConnect();
188
+ this.emit(EventName.VoiceConnect, data);
189
+ }
173
190
  #handleVoiceDisconnect(node, data) {
174
191
  const player = this.#players.get(data.guild_id);
175
192
  if (player?.node !== node)
176
193
  return;
194
+ if (data.reason === DisconnectReason.ConnectionLost) {
195
+ this.#handleConnectionLost(player);
196
+ return;
197
+ }
177
198
  player._onVoiceDisconnect();
178
- this.emit(EventName.VoiceDisconnect, data);
179
199
  player.node.decrementPlayerCount();
180
200
  this.#players.delete(data.guild_id);
201
+ this.emit(EventName.VoiceDisconnect, data);
202
+ }
203
+ #handleConnectionLost(player) {
204
+ if (player.voiceChannelId)
205
+ player.disconnect();
206
+ else
207
+ player._onVoiceDisconnect();
208
+ player.node.decrementPlayerCount();
209
+ this.#players.delete(player.guildId);
210
+ this.emit(EventName.VoiceDisconnect, {
211
+ guild_id: player.guildId,
212
+ reason: DisconnectReason.ConnectionLost
213
+ });
181
214
  }
182
215
  _onPlayerDestroy(guildId) {
183
216
  const player = this.#players.get(guildId);
@@ -226,4 +259,3 @@ export class LinkDaveClient extends EventEmitter {
226
259
  newNode.incrementPlayerCount();
227
260
  }
228
261
  }
229
- //# sourceMappingURL=client.js.map
@@ -0,0 +1,61 @@
1
+ import type { Filter, FiltersPayload } from "./types.js";
2
+ export declare class PlayerFilters {
3
+ #private;
4
+ /**
5
+ * Pitch multiplier applied on top of any preset pitch.
6
+ *
7
+ * - **Default:** `0` (no override — preset value is used as-is)
8
+ * - **Normal playback:** `1.0`
9
+ * - **Recommended range:** `0.5` – `2.0`
10
+ * - Values below `0` are clamped to `0`. Values above `2.0` will work
11
+ * but progressively degrade audio quality due to resampling artifacts.
12
+ *
13
+ * When a preset like {@link Filter.Nightcore} is active (which sets pitch
14
+ * to `1.3×`), this value is **multiplied** on top: e.g. `pitch = 0.5`
15
+ * with Nightcore → effective pitch = `1.3 × 0.5 = 0.65`.
16
+ */
17
+ get pitch(): number;
18
+ set pitch(value: number);
19
+ /**
20
+ * Speed multiplier applied on top of any preset speed.
21
+ *
22
+ * - **Default:** `0` (no override — preset value is used as-is)
23
+ * - **Normal playback:** `1.0`
24
+ * - **Recommended range:** `0.25` – `3.0`
25
+ * - Values below `0` are clamped to `0`. Extreme values (e.g. `>5.0`)
26
+ * will work but may cause audible quality loss since more source
27
+ * samples are consumed per output frame.
28
+ *
29
+ * When a preset like {@link Filter.Vaporwave} is active (which sets speed
30
+ * to `0.8×`), this value is **multiplied** on top: e.g. `speed = 1.25`
31
+ * with Vaporwave → effective speed = `0.8 × 1.25 = 1.0`.
32
+ */
33
+ get speed(): number;
34
+ set speed(value: number);
35
+ /**
36
+ * Toggle a filter on or off. If `enabled` is omitted the filter is
37
+ * flipped from its current state.
38
+ *
39
+ * **Preset filters** ({@link Filter.Nightcore}, {@link Filter.Vaporwave})
40
+ * adjust both speed and pitch by fixed amounts (1.3× and 0.8×
41
+ * respectively). Enabling both simultaneously multiplies their effects
42
+ * together (effective speed = `1.3 × 0.8 = 1.04`).
43
+ *
44
+ * **DSP filters** ({@link Filter.Tremolo}, {@link Filter.Vibrato},
45
+ * {@link Filter.Rotation}, {@link Filter.LowPass}) modify the audio
46
+ * signal in-place and can all be enabled simultaneously — they are
47
+ * applied in sequence (tremolo → vibrato → rotation → lowpass).
48
+ */
49
+ toggle(filter: Filter, enabled?: boolean): this;
50
+ /**
51
+ * @returns `true` if any filter is active or if pitch or speed are non-zero.
52
+ */
53
+ get active(): boolean;
54
+ /**
55
+ * @returns an array of all active filters.
56
+ */
57
+ get activeFilters(): Filter[];
58
+ get(filter: Filter): boolean | undefined;
59
+ clear(): void;
60
+ toPayload(): FiltersPayload | undefined;
61
+ }
@@ -0,0 +1,108 @@
1
+ export class PlayerFilters {
2
+ #state = new Map();
3
+ #pitch = 0;
4
+ #speed = 0;
5
+ /**
6
+ * Pitch multiplier applied on top of any preset pitch.
7
+ *
8
+ * - **Default:** `0` (no override — preset value is used as-is)
9
+ * - **Normal playback:** `1.0`
10
+ * - **Recommended range:** `0.5` – `2.0`
11
+ * - Values below `0` are clamped to `0`. Values above `2.0` will work
12
+ * but progressively degrade audio quality due to resampling artifacts.
13
+ *
14
+ * When a preset like {@link Filter.Nightcore} is active (which sets pitch
15
+ * to `1.3×`), this value is **multiplied** on top: e.g. `pitch = 0.5`
16
+ * with Nightcore → effective pitch = `1.3 × 0.5 = 0.65`.
17
+ */
18
+ get pitch() {
19
+ return this.#pitch;
20
+ }
21
+ set pitch(value) {
22
+ this.#pitch = Math.max(0, value);
23
+ }
24
+ /**
25
+ * Speed multiplier applied on top of any preset speed.
26
+ *
27
+ * - **Default:** `0` (no override — preset value is used as-is)
28
+ * - **Normal playback:** `1.0`
29
+ * - **Recommended range:** `0.25` – `3.0`
30
+ * - Values below `0` are clamped to `0`. Extreme values (e.g. `>5.0`)
31
+ * will work but may cause audible quality loss since more source
32
+ * samples are consumed per output frame.
33
+ *
34
+ * When a preset like {@link Filter.Vaporwave} is active (which sets speed
35
+ * to `0.8×`), this value is **multiplied** on top: e.g. `speed = 1.25`
36
+ * with Vaporwave → effective speed = `0.8 × 1.25 = 1.0`.
37
+ */
38
+ get speed() {
39
+ return this.#speed;
40
+ }
41
+ set speed(value) {
42
+ this.#speed = Math.max(0, value);
43
+ }
44
+ /**
45
+ * Toggle a filter on or off. If `enabled` is omitted the filter is
46
+ * flipped from its current state.
47
+ *
48
+ * **Preset filters** ({@link Filter.Nightcore}, {@link Filter.Vaporwave})
49
+ * adjust both speed and pitch by fixed amounts (1.3× and 0.8×
50
+ * respectively). Enabling both simultaneously multiplies their effects
51
+ * together (effective speed = `1.3 × 0.8 = 1.04`).
52
+ *
53
+ * **DSP filters** ({@link Filter.Tremolo}, {@link Filter.Vibrato},
54
+ * {@link Filter.Rotation}, {@link Filter.LowPass}) modify the audio
55
+ * signal in-place and can all be enabled simultaneously — they are
56
+ * applied in sequence (tremolo → vibrato → rotation → lowpass).
57
+ */
58
+ toggle(filter, enabled) {
59
+ const next = enabled ?? !this.#state.get(filter);
60
+ this.#state.set(filter, next);
61
+ return this;
62
+ }
63
+ /**
64
+ * @returns `true` if any filter is active or if pitch or speed are non-zero.
65
+ */
66
+ get active() {
67
+ for (const v of this.#state.values()) {
68
+ if (v)
69
+ return true;
70
+ }
71
+ return this.#pitch > 0 || this.#speed > 0;
72
+ }
73
+ /**
74
+ * @returns an array of all active filters.
75
+ */
76
+ get activeFilters() {
77
+ const result = [];
78
+ for (const [k, v] of this.#state)
79
+ if (v)
80
+ result.push(k);
81
+ return result;
82
+ }
83
+ get(filter) {
84
+ return this.#state.get(filter);
85
+ }
86
+ clear() {
87
+ this.#state.clear();
88
+ this.#pitch = 0;
89
+ this.#speed = 0;
90
+ }
91
+ toPayload() {
92
+ if (!this.active)
93
+ return undefined;
94
+ const enabled = [];
95
+ for (const [k, v] of this.#state) {
96
+ if (v)
97
+ enabled.push(k);
98
+ }
99
+ const payload = {};
100
+ if (enabled.length > 0)
101
+ payload.enabled = enabled;
102
+ if (this.#pitch > 0)
103
+ payload.pitch = this.#pitch;
104
+ if (this.#speed > 0)
105
+ payload.speed = this.#speed;
106
+ return payload;
107
+ }
108
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export * from "./client.js";
2
+ export * from "./filters.js";
2
3
  export * from "./node.js";
3
4
  export * from "./player.js";
4
5
  export * from "./queue.js";
5
6
  export * from "./rest.js";
6
7
  export * from "./types.js";
7
8
  export * from "./utils.js";
8
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export * from "./client.js";
2
+ export * from "./filters.js";
2
3
  export * from "./node.js";
3
4
  export * from "./player.js";
4
5
  export * from "./queue.js";
5
6
  export * from "./rest.js";
6
7
  export * from "./types.js";
7
8
  export * from "./utils.js";
8
- //# sourceMappingURL=index.js.map
package/dist/node.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { RESTClient } from "./rest.js";
3
- import type { Events, PlayPayload, SeekPayload, VoiceUpdatePayload, VolumePayload } from "./types.js";
3
+ import type { Events, PlayPayload, SeekPayload, VoiceUpdatePayload } from "./types.js";
4
4
  export interface NodeOptions {
5
5
  name: string;
6
6
  url: string;
@@ -23,7 +23,6 @@ export interface Node {
23
23
  }
24
24
  export declare class Node extends EventEmitter {
25
25
  #private;
26
- private static readonly NODE_PING_INTERVAL;
27
26
  readonly name: string;
28
27
  readonly url: string;
29
28
  readonly rest: RESTClient;
@@ -44,7 +43,5 @@ export declare class Node extends EventEmitter {
44
43
  sendResume(guildId: string): Promise<void>;
45
44
  sendStop(guildId: string): Promise<void>;
46
45
  sendSeek(guildId: string, data: SeekPayload): Promise<void>;
47
- sendVolume(guildId: string, data: VolumePayload): Promise<void>;
48
46
  sendDisconnect(guildId: string): Promise<void>;
49
47
  }
50
- //# sourceMappingURL=node.d.ts.map
package/dist/node.js CHANGED
@@ -11,7 +11,6 @@ export var NodeState;
11
11
  })(NodeState || (NodeState = {}));
12
12
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
13
13
  export class Node extends EventEmitter {
14
- static NODE_PING_INTERVAL = 30_000;
15
14
  name;
16
15
  url;
17
16
  rest;
@@ -20,7 +19,6 @@ export class Node extends EventEmitter {
20
19
  #sessionId = null;
21
20
  #reconnectAttempts = 0;
22
21
  #reconnectTimeout = null;
23
- #pingInterval = null;
24
22
  #state = NodeState.Disconnected;
25
23
  #playerCount = 0;
26
24
  constructor(options) {
@@ -53,7 +51,6 @@ export class Node extends EventEmitter {
53
51
  const onOpen = () => {
54
52
  this.#state = NodeState.Connected;
55
53
  this.#reconnectAttempts = 0;
56
- this.#startPingInterval();
57
54
  resolve(null);
58
55
  };
59
56
  const onError = (event) => {
@@ -70,7 +67,6 @@ export class Node extends EventEmitter {
70
67
  }
71
68
  disconnect() {
72
69
  this.#state = NodeState.Disconnected;
73
- this.#stopPingInterval();
74
70
  this.#stopReconnect();
75
71
  if (this.#ws) {
76
72
  this.#ws.close(1_000, "Client disconnect");
@@ -134,9 +130,6 @@ export class Node extends EventEmitter {
134
130
  case ServerOpCodes.VoiceDisconnect:
135
131
  this.emit(EventName.VoiceDisconnect, message.d);
136
132
  break;
137
- case ServerOpCodes.Pong:
138
- this.emit(EventName.Pong, undefined);
139
- break;
140
133
  case ServerOpCodes.Stats:
141
134
  this.#playerCount = message.d.players;
142
135
  this.#state = message.d.draining ? NodeState.Draining : NodeState.Connected;
@@ -153,7 +146,6 @@ export class Node extends EventEmitter {
153
146
  }
154
147
  #onClose(event) {
155
148
  this.#state = NodeState.Disconnected;
156
- this.#stopPingInterval();
157
149
  this.emit(EventName.Close, { code: event.code, reason: event.reason });
158
150
  if (event.code !== 1_000 && this.#options.autoReconnect && !this.draining && this.#reconnectAttempts < this.#options.maxReconnectAttempts) {
159
151
  this.#scheduleReconnect();
@@ -176,15 +168,6 @@ export class Node extends EventEmitter {
176
168
  }
177
169
  this.#reconnectAttempts = 0;
178
170
  }
179
- #startPingInterval() {
180
- this.#pingInterval = setInterval(() => this.#send(ClientOpCodes.Ping, undefined), Node.NODE_PING_INTERVAL);
181
- }
182
- #stopPingInterval() {
183
- if (!this.#pingInterval)
184
- return;
185
- clearInterval(this.#pingInterval);
186
- this.#pingInterval = null;
187
- }
188
171
  sendVoiceUpdate(data) {
189
172
  this.#send(ClientOpCodes.VoiceUpdate, data);
190
173
  }
@@ -206,9 +189,6 @@ export class Node extends EventEmitter {
206
189
  async sendSeek(guildId, data) {
207
190
  await this.rest.post(Routes.seek(this.#requireSession(), guildId), data);
208
191
  }
209
- async sendVolume(guildId, data) {
210
- await this.rest.patch(Routes.volume(this.#requireSession(), guildId), data);
211
- }
212
192
  async sendDisconnect(guildId) {
213
193
  await this.rest.delete(Routes.disconnect(this.#requireSession(), guildId));
214
194
  }
@@ -226,4 +206,3 @@ export class Node extends EventEmitter {
226
206
  this.#ws.send(JSON.stringify(message));
227
207
  }
228
208
  }
229
- //# sourceMappingURL=node.js.map
package/dist/player.d.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  import { type GatewayVoiceServerUpdateDispatchData, type GatewayVoiceStateUpdateDispatchData } from "discord-api-types/v10";
2
2
  import type { LinkDaveClient } from "./client.js";
3
+ import { PlayerFilters } from "./filters.js";
3
4
  import type { Node } from "./node.js";
4
5
  import { Queue } from "./queue.js";
5
- import type { MigrateReadyPayload, PlayerUpdatePayload, TrackEndPayload, TrackInfo, TrackStartPayload, VoiceConnectPayload } from "./types.js";
6
+ import type { FiltersPayload, MigrateReadyPayload, PlayerUpdatePayload, TrackEndPayload, TrackInfo, TrackStartPayload, VoiceConnectPayload } from "./types.js";
6
7
  import { PlayerState } from "./types.js";
7
8
  export interface PlayOptions {
8
9
  startTime?: number;
9
- volume?: number;
10
+ requesterId?: string;
11
+ filters?: FiltersPayload;
10
12
  }
11
13
  export interface PlayerOptions {
12
14
  voiceChannelId?: string;
13
15
  selfMute?: boolean;
14
16
  selfDeaf?: boolean;
17
+ inactivityTimeout?: number;
15
18
  }
16
19
  export type RawVoiceStateUpdate = Pick<GatewayVoiceStateUpdateDispatchData, "user_id" | "channel_id" | "session_id">;
17
20
  export type RawVoiceServerUpdate = Pick<GatewayVoiceServerUpdateDispatchData, "token" | "guild_id" | "endpoint">;
@@ -22,14 +25,13 @@ export declare class Player {
22
25
  get guildId(): string;
23
26
  get voiceChannelId(): string | null;
24
27
  get state(): PlayerState;
25
- get position(): number;
26
- get volume(): number;
27
28
  get current(): TrackInfo | null;
28
29
  get node(): Node;
29
30
  get queue(): Queue;
30
31
  get playing(): boolean;
31
32
  get paused(): boolean;
32
33
  get connected(): boolean;
34
+ get filters(): PlayerFilters;
33
35
  connect(channelId?: string, timeoutMs?: number): Promise<VoiceConnectPayload>;
34
36
  disconnect(): void;
35
37
  handleVoiceStateUpdate(data: RawVoiceStateUpdate): Promise<void>;
@@ -39,13 +41,12 @@ export declare class Player {
39
41
  resume(): Promise<void>;
40
42
  stop(): Promise<void>;
41
43
  seek(position: number): Promise<void>;
42
- setVolume(volume: number): Promise<void>;
43
44
  destroy(): Promise<void>;
44
45
  moveNode(targetNode: Node): Promise<unknown>;
45
- _updateState(data: PlayerUpdatePayload): void;
46
+ _onPlayerUpdate(data: PlayerUpdatePayload): void;
46
47
  _onTrackStart(data: TrackStartPayload): void;
47
48
  _onTrackEnd(data: TrackEndPayload): void;
49
+ _onVoiceConnect(): void;
48
50
  _onVoiceDisconnect(): void;
49
51
  _onMigrateReady(data: MigrateReadyPayload): void;
50
52
  }
51
- //# sourceMappingURL=player.d.ts.map