linkdave 0.1.6-dev.d8a9fae → 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;
package/dist/client.js CHANGED
@@ -4,6 +4,7 @@ import { Node } from "./node.js";
4
4
  import { Player } from "./player.js";
5
5
  import { DisconnectReason, EventName, ManagerEventName } from "./types.js";
6
6
  const CLIENT_ID_REGEX = /^\d{15,21}$/;
7
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
7
8
  export class LinkDaveClient extends EventEmitter {
8
9
  #clientId;
9
10
  #sendToShard;
@@ -69,13 +70,25 @@ export class LinkDaveClient extends EventEmitter {
69
70
  }
70
71
  return bestNode;
71
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
+ }
72
83
  getPlayer(guildId, options) {
73
84
  let player = this.#players.get(guildId);
74
85
  if (player)
75
86
  return player;
76
- const node = this.getBestNode();
87
+ const node = this.getPreferredNode(options?.nodeId);
77
88
  if (!node) {
78
- 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");
79
92
  }
80
93
  player = new Player(this, guildId, node, options);
81
94
  this.#players.set(guildId, player);
@@ -132,9 +145,8 @@ export class LinkDaveClient extends EventEmitter {
132
145
  node.on(EventName.TrackEnd, (data) => this.#handleTrackEnd(node, data));
133
146
  node.on(EventName.TrackError, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.TrackError, data));
134
147
  node.on(EventName.QueueError, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.QueueError, data));
135
- node.on(EventName.VoiceConnect, (data) => this.#forwardPlayerEvent(node, data.guild_id, EventName.VoiceConnect, data));
148
+ node.on(EventName.VoiceConnect, (data) => this.#handleVoiceConnect(node, data));
136
149
  node.on(EventName.VoiceDisconnect, (data) => this.#handleVoiceDisconnect(node, data));
137
- node.on(EventName.Pong, () => this.emit(EventName.Pong, undefined));
138
150
  node.on(EventName.Stats, (data) => this.emit(EventName.Stats, data));
139
151
  node.on(EventName.NodeDraining, (data) => this.#handleNodeDraining(node, data));
140
152
  node.on(EventName.MigrateReady, (data) => this.#handleMigrateReady(node, data));
@@ -151,7 +163,7 @@ export class LinkDaveClient extends EventEmitter {
151
163
  const player = this.#players.get(data.guild_id);
152
164
  if (player?.node !== node)
153
165
  return;
154
- player._updateState(data);
166
+ player._onPlayerUpdate(data);
155
167
  this.emit(EventName.PlayerUpdate, data);
156
168
  }
157
169
  #handleTrackStart(node, data) {
@@ -168,6 +180,13 @@ export class LinkDaveClient extends EventEmitter {
168
180
  player._onTrackEnd(data);
169
181
  this.emit(EventName.TrackEnd, data);
170
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
+ }
171
190
  #handleVoiceDisconnect(node, data) {
172
191
  const player = this.#players.get(data.guild_id);
173
192
  if (player?.node !== node)
@@ -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,4 +1,5 @@
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";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
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";
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,6 +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
  }
package/dist/node.js CHANGED
@@ -9,8 +9,8 @@ export var NodeState;
9
9
  NodeState[NodeState["Connected"] = 2] = "Connected";
10
10
  NodeState[NodeState["Draining"] = 3] = "Draining";
11
11
  })(NodeState || (NodeState = {}));
12
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
12
13
  export class Node extends EventEmitter {
13
- static NODE_PING_INTERVAL = 30_000;
14
14
  name;
15
15
  url;
16
16
  rest;
@@ -19,7 +19,6 @@ export class Node extends EventEmitter {
19
19
  #sessionId = null;
20
20
  #reconnectAttempts = 0;
21
21
  #reconnectTimeout = null;
22
- #pingInterval = null;
23
22
  #state = NodeState.Disconnected;
24
23
  #playerCount = 0;
25
24
  constructor(options) {
@@ -52,7 +51,6 @@ export class Node extends EventEmitter {
52
51
  const onOpen = () => {
53
52
  this.#state = NodeState.Connected;
54
53
  this.#reconnectAttempts = 0;
55
- this.#startPingInterval();
56
54
  resolve(null);
57
55
  };
58
56
  const onError = (event) => {
@@ -69,7 +67,6 @@ export class Node extends EventEmitter {
69
67
  }
70
68
  disconnect() {
71
69
  this.#state = NodeState.Disconnected;
72
- this.#stopPingInterval();
73
70
  this.#stopReconnect();
74
71
  if (this.#ws) {
75
72
  this.#ws.close(1_000, "Client disconnect");
@@ -106,6 +103,7 @@ export class Node extends EventEmitter {
106
103
  this.#handleMessage(message);
107
104
  }
108
105
  catch {
106
+ // Invalid messages are silently ignored
109
107
  }
110
108
  }
111
109
  #handleMessage(message) {
@@ -132,9 +130,6 @@ export class Node extends EventEmitter {
132
130
  case ServerOpCodes.VoiceDisconnect:
133
131
  this.emit(EventName.VoiceDisconnect, message.d);
134
132
  break;
135
- case ServerOpCodes.Pong:
136
- this.emit(EventName.Pong, undefined);
137
- break;
138
133
  case ServerOpCodes.Stats:
139
134
  this.#playerCount = message.d.players;
140
135
  this.#state = message.d.draining ? NodeState.Draining : NodeState.Connected;
@@ -151,7 +146,6 @@ export class Node extends EventEmitter {
151
146
  }
152
147
  #onClose(event) {
153
148
  this.#state = NodeState.Disconnected;
154
- this.#stopPingInterval();
155
149
  this.emit(EventName.Close, { code: event.code, reason: event.reason });
156
150
  if (event.code !== 1_000 && this.#options.autoReconnect && !this.draining && this.#reconnectAttempts < this.#options.maxReconnectAttempts) {
157
151
  this.#scheduleReconnect();
@@ -174,15 +168,6 @@ export class Node extends EventEmitter {
174
168
  }
175
169
  this.#reconnectAttempts = 0;
176
170
  }
177
- #startPingInterval() {
178
- this.#pingInterval = setInterval(() => this.#send(ClientOpCodes.Ping, undefined), Node.NODE_PING_INTERVAL);
179
- }
180
- #stopPingInterval() {
181
- if (!this.#pingInterval)
182
- return;
183
- clearInterval(this.#pingInterval);
184
- this.#pingInterval = null;
185
- }
186
171
  sendVoiceUpdate(data) {
187
172
  this.#send(ClientOpCodes.VoiceUpdate, data);
188
173
  }
@@ -204,9 +189,6 @@ export class Node extends EventEmitter {
204
189
  async sendSeek(guildId, data) {
205
190
  await this.rest.post(Routes.seek(this.#requireSession(), guildId), data);
206
191
  }
207
- async sendVolume(guildId, data) {
208
- await this.rest.patch(Routes.volume(this.#requireSession(), guildId), data);
209
- }
210
192
  async sendDisconnect(guildId) {
211
193
  await this.rest.delete(Routes.disconnect(this.#requireSession(), guildId));
212
194
  }
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,12 +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
  }
package/dist/player.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GatewayOpcodes } from "discord-api-types/v10";
2
+ import { PlayerFilters } from "./filters.js";
2
3
  import { Queue } from "./queue.js";
3
4
  import { DisconnectReason, EventName, PlayerState, TrackEndReason } from "./types.js";
4
5
  import { unwrap } from "./utils.js";
@@ -7,18 +8,19 @@ export class Player {
7
8
  #client;
8
9
  #guildId;
9
10
  #queue;
11
+ #filters = new PlayerFilters();
10
12
  #node;
11
13
  #voiceChannelId = null;
12
14
  #selfMute;
13
15
  #selfDeaf;
14
16
  #state = PlayerState.Idle;
15
- #position = 0;
16
- #volume = 100;
17
17
  #current = null;
18
18
  #voiceState = null;
19
19
  #pendingVoice = null;
20
20
  #migrationTarget = null;
21
21
  #migrationResolve = null;
22
+ #inactivityTimeout;
23
+ #inactivityTimer = null;
22
24
  constructor(client, guildId, node, options) {
23
25
  this.#client = client;
24
26
  this.#guildId = guildId;
@@ -27,6 +29,7 @@ export class Player {
27
29
  this.#voiceChannelId = options?.voiceChannelId ?? null;
28
30
  this.#selfMute = options?.selfMute ?? false;
29
31
  this.#selfDeaf = options?.selfDeaf ?? true;
32
+ this.#inactivityTimeout = options?.inactivityTimeout ?? 0;
30
33
  }
31
34
  get guildId() {
32
35
  return this.#guildId;
@@ -37,12 +40,6 @@ export class Player {
37
40
  get state() {
38
41
  return this.#state;
39
42
  }
40
- get position() {
41
- return this.#position;
42
- }
43
- get volume() {
44
- return this.#volume;
45
- }
46
43
  get current() {
47
44
  return this.#current;
48
45
  }
@@ -61,6 +58,9 @@ export class Player {
61
58
  get connected() {
62
59
  return this.#voiceState !== null;
63
60
  }
61
+ get filters() {
62
+ return this.#filters;
63
+ }
64
64
  connect(channelId, timeoutMs = Player.CONNECT_TIMEOUT) {
65
65
  const targetChannel = channelId ?? this.#voiceChannelId;
66
66
  if (!targetChannel) {
@@ -151,7 +151,7 @@ export class Player {
151
151
  this.#pendingVoice ??= {};
152
152
  const endpoint = data.endpoint || this.#pendingVoice.serverEvent?.endpoint;
153
153
  if (!endpoint)
154
- throw new Error("Missing voice server endpoint");
154
+ throw new Error("Missing voice server endpoint"); // TODO
155
155
  this.#pendingVoice.serverEvent = {
156
156
  token: data.token,
157
157
  guild_id: data.guild_id,
@@ -184,10 +184,12 @@ export class Player {
184
184
  await this.#sendPlay(url, options);
185
185
  }
186
186
  async #sendPlay(url, options = {}) {
187
+ const filters = options.filters ?? this.#filters.toPayload();
187
188
  await this.#node.sendPlay(this.#guildId, {
188
189
  url,
189
190
  ...(options.startTime !== undefined && { start_time: options.startTime }),
190
- ...(options.volume !== undefined && { volume: options.volume })
191
+ ...(options.requesterId !== undefined && { requester_id: options.requesterId }),
192
+ ...(filters !== undefined && { filters })
191
193
  });
192
194
  }
193
195
  async pause() {
@@ -199,17 +201,12 @@ export class Player {
199
201
  async stop() {
200
202
  this.#queue._deactivate();
201
203
  await this.#node.sendStop(this.#guildId);
202
- this.#current = null;
203
204
  this.#state = PlayerState.Idle;
204
- this.#position = 0;
205
+ this.#current = null;
205
206
  }
206
207
  async seek(position) {
207
208
  await this.#node.sendSeek(this.#guildId, { position });
208
209
  }
209
- async setVolume(volume) {
210
- this.#volume = Math.max(0, Math.min(1_000, volume));
211
- await this.#node.sendVolume(this.#guildId, { volume: this.#volume });
212
- }
213
210
  async destroy() {
214
211
  this.disconnect();
215
212
  if (!this.#node.connected) {
@@ -252,20 +249,31 @@ export class Player {
252
249
  this.#migrationResolve = resolve;
253
250
  });
254
251
  }
255
- _updateState(data) {
252
+ _onPlayerUpdate(data) {
253
+ if (data.state === PlayerState.Playing)
254
+ this.#stopTimer();
255
+ else if (this.#state === PlayerState.Playing)
256
+ this.#startTimer();
256
257
  this.#state = data.state;
257
- this.#position = data.position;
258
- this.#volume = data.volume;
259
258
  }
260
259
  _onTrackStart(data) {
261
260
  this.#current = data.track;
261
+ this.#state = PlayerState.Playing;
262
+ this.#stopTimer();
262
263
  }
263
264
  _onTrackEnd(data) {
264
265
  if (!this.#queue.active || this.#queue.size === 0) {
265
- this.#current = null;
266
266
  this.#state = PlayerState.Idle;
267
+ this.#current = null;
268
+ this.#startTimer();
267
269
  }
268
- this.#queue._onTrackEnd(data.reason !== TrackEndReason.Stopped);
270
+ this.#queue._onTrackEnd(data.reason !== TrackEndReason.Stopped && data.reason !== TrackEndReason.Replaced);
271
+ }
272
+ _onVoiceConnect() {
273
+ if (this.#state !== PlayerState.Connecting)
274
+ return;
275
+ this.#state = PlayerState.Idle;
276
+ this.#startTimer();
269
277
  }
270
278
  #cleanup() {
271
279
  this.#voiceChannelId = null;
@@ -274,7 +282,7 @@ export class Player {
274
282
  this.#pendingVoice = null;
275
283
  this.#state = PlayerState.Idle;
276
284
  this.#current = null;
277
- this.#position = 0;
285
+ this.#stopTimer();
278
286
  }
279
287
  _onVoiceDisconnect() {
280
288
  this.#cleanup();
@@ -285,6 +293,7 @@ export class Player {
285
293
  }
286
294
  const targetNode = this.#migrationTarget;
287
295
  const oldNode = this.#node;
296
+ // Don't send disconnect to old node - we're migrating
288
297
  this.#client._updatePlayerNode(this.#guildId, oldNode, targetNode);
289
298
  this.#node = targetNode;
290
299
  if (this.#voiceState) {
@@ -297,16 +306,16 @@ export class Player {
297
306
  });
298
307
  }
299
308
  if (data.state === PlayerState.Playing && data.url) {
300
- const playData = {
301
- url: data.url,
302
- start_time: data.position,
303
- volume: data.volume
304
- };
305
309
  const onVoiceConnect = (event) => {
306
310
  if (event.guild_id !== this.#guildId)
307
311
  return;
308
312
  this.#node.off(EventName.VoiceConnect, onVoiceConnect);
309
- void this.#node.sendPlay(this.#guildId, playData);
313
+ void this.#node.sendPlay(this.#guildId, {
314
+ url: data.url,
315
+ start_time: data.position,
316
+ ...(data.requester_id !== undefined && { requester_id: data.requester_id }),
317
+ ...(data.filters !== undefined && { filters: data.filters })
318
+ });
310
319
  };
311
320
  this.#node.on(EventName.VoiceConnect, onVoiceConnect);
312
321
  }
@@ -316,4 +325,28 @@ export class Player {
316
325
  this.#migrationResolve(null);
317
326
  this.#migrationResolve = null;
318
327
  }
328
+ #startTimer() {
329
+ if (this.#inactivityTimeout <= 0)
330
+ return;
331
+ this.#stopTimer();
332
+ this.#inactivityTimer = setTimeout(() => {
333
+ if (this.#state === PlayerState.Playing)
334
+ return;
335
+ this.disconnect();
336
+ if (this.#node.connected) {
337
+ void this.#node.sendDisconnect(this.#guildId);
338
+ }
339
+ this.#client._onPlayerDestroy(this.#guildId);
340
+ this.#client.emit(EventName.VoiceDisconnect, {
341
+ guild_id: this.#guildId,
342
+ reason: DisconnectReason.Inactivity
343
+ });
344
+ }, this.#inactivityTimeout);
345
+ }
346
+ #stopTimer() {
347
+ if (this.#inactivityTimer === null)
348
+ return;
349
+ clearTimeout(this.#inactivityTimer);
350
+ this.#inactivityTimer = null;
351
+ }
319
352
  }
package/dist/queue.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import type { Player } from "./player.js";
1
+ import type { Player, PlayOptions } from "./player.js";
2
+ export interface QueueItem {
3
+ uri: string;
4
+ options: PlayOptions;
5
+ }
2
6
  export declare class Queue {
3
7
  #private;
4
8
  constructor(player: Player);
5
- add(uri: string): this;
9
+ add(uri: string, options?: PlayOptions): this;
6
10
  start(): Promise<void>;
7
11
  skip(): Promise<void>;
8
- remove(index: number): string | undefined;
12
+ remove(index: number): QueueItem | undefined;
9
13
  clear(): void;
10
- get tracks(): readonly string[];
14
+ get tracks(): readonly QueueItem[];
11
15
  get size(): number;
12
16
  get active(): boolean;
13
17
  _onTrackEnd(finished: boolean): void;
package/dist/queue.js CHANGED
@@ -6,8 +6,8 @@ export class Queue {
6
6
  constructor(player) {
7
7
  this.#player = player;
8
8
  }
9
- add(uri) {
10
- this.#tracks.push(uri);
9
+ add(uri, options = {}) {
10
+ this.#tracks.push({ uri, options });
11
11
  return this;
12
12
  }
13
13
  async start() {
@@ -56,10 +56,10 @@ export class Queue {
56
56
  if (!item)
57
57
  return;
58
58
  this.#player
59
- .play(item, undefined, true)
59
+ .play(item.uri, item.options, true)
60
60
  .then(() => null, (error_) => {
61
61
  const error = error_ instanceof Error ? error_ : new Error(String(error_));
62
- const payload = { guild_id: this.#player.guildId, url: item, error };
62
+ const payload = { guild_id: this.#player.guildId, url: item.uri, error };
63
63
  this.#player.node.emit(EventName.QueueError, payload);
64
64
  this._onTrackEnd(true);
65
65
  });
@@ -71,6 +71,6 @@ export class Queue {
71
71
  const item = this.#tracks.shift();
72
72
  if (!item)
73
73
  return;
74
- await this.#player.play(item, undefined, true);
74
+ await this.#player.play(item.uri, item.options, true);
75
75
  }
76
76
  }
package/dist/types.d.ts CHANGED
@@ -1,21 +1,19 @@
1
1
  import type { Node } from "./node.js";
2
2
  export declare enum ClientOpCodes {
3
- Ping = 0,
4
- VoiceUpdate = 1,
5
- PlayerMigrate = 2
3
+ VoiceUpdate = 0,
4
+ PlayerMigrate = 1
6
5
  }
7
6
  export declare enum ServerOpCodes {
8
- Pong = 0,
9
- Ready = 1,
10
- VoiceConnect = 2,
11
- VoiceDisconnect = 3,
12
- PlayerUpdate = 4,
13
- TrackStart = 5,
14
- TrackEnd = 6,
15
- TrackError = 7,
16
- Stats = 8,
17
- NodeDraining = 9,
18
- MigrateReady = 10
7
+ Ready = 0,
8
+ VoiceConnect = 1,
9
+ VoiceDisconnect = 2,
10
+ PlayerUpdate = 3,
11
+ TrackStart = 4,
12
+ TrackEnd = 5,
13
+ TrackError = 6,
14
+ Stats = 7,
15
+ NodeDraining = 8,
16
+ MigrateReady = 9
19
17
  }
20
18
  export declare enum TrackEndReason {
21
19
  Finished = "finished",
@@ -29,6 +27,25 @@ export declare enum PlayerState {
29
27
  Paused = "paused",
30
28
  Connecting = "connecting"
31
29
  }
30
+ export declare enum Filter {
31
+ /** Slows and lowers pitch (speed ×0.8, pitch ×0.8). */
32
+ Vaporwave = 0,
33
+ /** Speeds up and raises pitch (speed ×1.3, pitch ×1.3). */
34
+ Nightcore = 1,
35
+ /** Rotates audio around the stereo field at 0.2 Hz. */
36
+ Rotation = 2,
37
+ /** Oscillates volume at 4 Hz with 0.6 depth. */
38
+ Tremolo = 3,
39
+ /** Oscillates pitch at 4 Hz with 0.5 depth. */
40
+ Vibrato = 4,
41
+ /** Suppresses high frequencies (smoothing factor 20). */
42
+ LowPass = 5
43
+ }
44
+ export interface FiltersPayload {
45
+ enabled?: Filter[];
46
+ pitch?: number;
47
+ speed?: number;
48
+ }
32
49
  export type ServerMessage = {
33
50
  op: ServerOpCodes.Ready;
34
51
  d: ReadyPayload;
@@ -50,9 +67,6 @@ export type ServerMessage = {
50
67
  } | {
51
68
  op: ServerOpCodes.VoiceDisconnect;
52
69
  d: VoiceDisconnectPayload;
53
- } | {
54
- op: ServerOpCodes.Pong;
55
- d?: undefined;
56
70
  } | {
57
71
  op: ServerOpCodes.Stats;
58
72
  d: StatsPayload;
@@ -66,9 +80,6 @@ export type ServerMessage = {
66
80
  export type ClientMessage = {
67
81
  op: ClientOpCodes.VoiceUpdate;
68
82
  d: VoiceUpdatePayload;
69
- } | {
70
- op: ClientOpCodes.Ping;
71
- d?: undefined;
72
83
  } | {
73
84
  op: ClientOpCodes.PlayerMigrate;
74
85
  d: PlayerMigratePayload;
@@ -88,7 +99,8 @@ export interface VoiceUpdatePayload {
88
99
  export interface PlayPayload {
89
100
  url: string;
90
101
  start_time?: number;
91
- volume?: number;
102
+ requester_id?: string;
103
+ filters?: FiltersPayload;
92
104
  }
93
105
  export interface GuildPayload {
94
106
  guild_id: string;
@@ -96,9 +108,6 @@ export interface GuildPayload {
96
108
  export interface SeekPayload {
97
109
  position: number;
98
110
  }
99
- export interface VolumePayload {
100
- volume: number;
101
- }
102
111
  export interface ReadyPayload {
103
112
  session_id: string;
104
113
  resumed: boolean;
@@ -106,13 +115,12 @@ export interface ReadyPayload {
106
115
  export interface PlayerUpdatePayload {
107
116
  guild_id: string;
108
117
  state: PlayerState;
109
- position: number;
110
- volume: number;
111
118
  }
112
119
  export interface TrackInfo {
113
120
  url: string;
114
121
  title?: string;
115
122
  duration: number;
123
+ requester_id?: string;
116
124
  }
117
125
  export interface TrackStartPayload {
118
126
  guild_id: string;
@@ -136,7 +144,8 @@ export interface QueueErrorPayload {
136
144
  export declare enum DisconnectReason {
137
145
  ConnectionLost = "connection_lost",
138
146
  ConnectionFailed = "connection_failed",
139
- Requested = "requested"
147
+ Requested = "requested",
148
+ Inactivity = "inactivity"
140
149
  }
141
150
  export interface VoiceConnectPayload {
142
151
  guild_id: string;
@@ -164,8 +173,9 @@ export interface MigrateReadyPayload {
164
173
  guild_id: string;
165
174
  url: string;
166
175
  position: number;
167
- volume: number;
168
176
  state: PlayerState;
177
+ requester_id?: string;
178
+ filters?: FiltersPayload;
169
179
  }
170
180
  export interface ClosePayload {
171
181
  code: number;
@@ -180,7 +190,6 @@ export declare enum EventName {
180
190
  QueueError = "queueError",
181
191
  VoiceConnect = "voiceConnect",
182
192
  VoiceDisconnect = "voiceDisconnect",
183
- Pong = "pong",
184
193
  Stats = "stats",
185
194
  NodeDraining = "nodeDraining",
186
195
  MigrateReady = "migrateReady",
@@ -196,7 +205,6 @@ export interface Events {
196
205
  [EventName.QueueError]: QueueErrorPayload;
197
206
  [EventName.VoiceConnect]: VoiceConnectPayload;
198
207
  [EventName.VoiceDisconnect]: VoiceDisconnectPayload;
199
- [EventName.Pong]: undefined;
200
208
  [EventName.Stats]: StatsPayload;
201
209
  [EventName.NodeDraining]: NodeDrainingPayload;
202
210
  [EventName.MigrateReady]: MigrateReadyPayload;
@@ -230,6 +238,5 @@ export declare const Routes: {
230
238
  readonly resume: (sessionId: string, guildId: string) => `/sessions/${string}/players/${string}/resume`;
231
239
  readonly stop: (sessionId: string, guildId: string) => `/sessions/${string}/players/${string}/stop`;
232
240
  readonly seek: (sessionId: string, guildId: string) => `/sessions/${string}/players/${string}/seek`;
233
- readonly volume: (sessionId: string, guildId: string) => `/sessions/${string}/players/${string}/volume`;
234
241
  readonly disconnect: (sessionId: string, guildId: string) => `/sessions/${string}/players/${string}`;
235
242
  };
package/dist/types.js CHANGED
@@ -1,22 +1,20 @@
1
1
  export var ClientOpCodes;
2
2
  (function (ClientOpCodes) {
3
- ClientOpCodes[ClientOpCodes["Ping"] = 0] = "Ping";
4
- ClientOpCodes[ClientOpCodes["VoiceUpdate"] = 1] = "VoiceUpdate";
5
- ClientOpCodes[ClientOpCodes["PlayerMigrate"] = 2] = "PlayerMigrate";
3
+ ClientOpCodes[ClientOpCodes["VoiceUpdate"] = 0] = "VoiceUpdate";
4
+ ClientOpCodes[ClientOpCodes["PlayerMigrate"] = 1] = "PlayerMigrate";
6
5
  })(ClientOpCodes || (ClientOpCodes = {}));
7
6
  export var ServerOpCodes;
8
7
  (function (ServerOpCodes) {
9
- ServerOpCodes[ServerOpCodes["Pong"] = 0] = "Pong";
10
- ServerOpCodes[ServerOpCodes["Ready"] = 1] = "Ready";
11
- ServerOpCodes[ServerOpCodes["VoiceConnect"] = 2] = "VoiceConnect";
12
- ServerOpCodes[ServerOpCodes["VoiceDisconnect"] = 3] = "VoiceDisconnect";
13
- ServerOpCodes[ServerOpCodes["PlayerUpdate"] = 4] = "PlayerUpdate";
14
- ServerOpCodes[ServerOpCodes["TrackStart"] = 5] = "TrackStart";
15
- ServerOpCodes[ServerOpCodes["TrackEnd"] = 6] = "TrackEnd";
16
- ServerOpCodes[ServerOpCodes["TrackError"] = 7] = "TrackError";
17
- ServerOpCodes[ServerOpCodes["Stats"] = 8] = "Stats";
18
- ServerOpCodes[ServerOpCodes["NodeDraining"] = 9] = "NodeDraining";
19
- ServerOpCodes[ServerOpCodes["MigrateReady"] = 10] = "MigrateReady";
8
+ ServerOpCodes[ServerOpCodes["Ready"] = 0] = "Ready";
9
+ ServerOpCodes[ServerOpCodes["VoiceConnect"] = 1] = "VoiceConnect";
10
+ ServerOpCodes[ServerOpCodes["VoiceDisconnect"] = 2] = "VoiceDisconnect";
11
+ ServerOpCodes[ServerOpCodes["PlayerUpdate"] = 3] = "PlayerUpdate";
12
+ ServerOpCodes[ServerOpCodes["TrackStart"] = 4] = "TrackStart";
13
+ ServerOpCodes[ServerOpCodes["TrackEnd"] = 5] = "TrackEnd";
14
+ ServerOpCodes[ServerOpCodes["TrackError"] = 6] = "TrackError";
15
+ ServerOpCodes[ServerOpCodes["Stats"] = 7] = "Stats";
16
+ ServerOpCodes[ServerOpCodes["NodeDraining"] = 8] = "NodeDraining";
17
+ ServerOpCodes[ServerOpCodes["MigrateReady"] = 9] = "MigrateReady";
20
18
  })(ServerOpCodes || (ServerOpCodes = {}));
21
19
  export var TrackEndReason;
22
20
  (function (TrackEndReason) {
@@ -32,11 +30,27 @@ export var PlayerState;
32
30
  PlayerState["Paused"] = "paused";
33
31
  PlayerState["Connecting"] = "connecting";
34
32
  })(PlayerState || (PlayerState = {}));
33
+ export var Filter;
34
+ (function (Filter) {
35
+ /** Slows and lowers pitch (speed ×0.8, pitch ×0.8). */
36
+ Filter[Filter["Vaporwave"] = 0] = "Vaporwave";
37
+ /** Speeds up and raises pitch (speed ×1.3, pitch ×1.3). */
38
+ Filter[Filter["Nightcore"] = 1] = "Nightcore";
39
+ /** Rotates audio around the stereo field at 0.2 Hz. */
40
+ Filter[Filter["Rotation"] = 2] = "Rotation";
41
+ /** Oscillates volume at 4 Hz with 0.6 depth. */
42
+ Filter[Filter["Tremolo"] = 3] = "Tremolo";
43
+ /** Oscillates pitch at 4 Hz with 0.5 depth. */
44
+ Filter[Filter["Vibrato"] = 4] = "Vibrato";
45
+ /** Suppresses high frequencies (smoothing factor 20). */
46
+ Filter[Filter["LowPass"] = 5] = "LowPass";
47
+ })(Filter || (Filter = {}));
35
48
  export var DisconnectReason;
36
49
  (function (DisconnectReason) {
37
50
  DisconnectReason["ConnectionLost"] = "connection_lost";
38
51
  DisconnectReason["ConnectionFailed"] = "connection_failed";
39
52
  DisconnectReason["Requested"] = "requested";
53
+ DisconnectReason["Inactivity"] = "inactivity";
40
54
  })(DisconnectReason || (DisconnectReason = {}));
41
55
  export var EventName;
42
56
  (function (EventName) {
@@ -48,7 +62,6 @@ export var EventName;
48
62
  EventName["QueueError"] = "queueError";
49
63
  EventName["VoiceConnect"] = "voiceConnect";
50
64
  EventName["VoiceDisconnect"] = "voiceDisconnect";
51
- EventName["Pong"] = "pong";
52
65
  EventName["Stats"] = "stats";
53
66
  EventName["NodeDraining"] = "nodeDraining";
54
67
  EventName["MigrateReady"] = "migrateReady";
@@ -67,6 +80,5 @@ export const Routes = {
67
80
  resume: (sessionId, guildId) => `/sessions/${sessionId}/players/${guildId}/resume`,
68
81
  stop: (sessionId, guildId) => `/sessions/${sessionId}/players/${guildId}/stop`,
69
82
  seek: (sessionId, guildId) => `/sessions/${sessionId}/players/${guildId}/seek`,
70
- volume: (sessionId, guildId) => `/sessions/${sessionId}/players/${guildId}/volume`,
71
83
  disconnect: (sessionId, guildId) => `/sessions/${sessionId}/players/${guildId}`
72
84
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkdave",
3
- "version": "0.1.6-dev.d8a9fae",
3
+ "version": "0.2.0-dev.67c58c7",
4
4
  "author": "Luna Seemann <luna@wamellow.com> (http://shi.gg)",
5
5
  "description": "TypeScript client library for linkdave Discord audio streaming server",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  "packageManager": "bun@1.3.10",
32
32
  "devDependencies": {
33
33
  "@mwlica/eslint": "^1.2.0",
34
- "@types/node": "^25.6.0",
34
+ "@types/node": "^25.9.1",
35
35
  "typescript": "^6.0.3",
36
- "typescript-eslint": "^8.59.0"
36
+ "typescript-eslint": "^8.60.0"
37
37
  },
38
38
  "keywords": [
39
39
  "discord",
@@ -44,6 +44,6 @@
44
44
  ],
45
45
  "license": "AGPL-3.0-only",
46
46
  "dependencies": {
47
- "discord-api-types": "^0.38.47"
47
+ "discord-api-types": "^0.38.48"
48
48
  }
49
49
  }