magmastream 2.9.0-dev.30 → 2.9.0-dev.32

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.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Collection } from '@discordjs/collection';
2
2
  import { GatewayVoiceStateUpdate } from 'discord-api-types/v10';
3
3
  import { EventEmitter } from 'events';
4
- import WebSocket from 'ws';
4
+ import WebSocket, { WebSocket as WebSocket$1 } from 'ws';
5
5
  import { User, ClientUser, Message, Client } from 'discord.js';
6
6
  import { Redis } from 'ioredis';
7
7
  import { Client as Client$1 } from 'eris';
@@ -28,6 +28,8 @@ declare class Rest {
28
28
  private readonly url;
29
29
  /** The Manager instance. */
30
30
  manager: Manager;
31
+ /** Whether the node is a NodeLink. */
32
+ isNodeLink: boolean;
31
33
  constructor(node: Node$1, manager: Manager);
32
34
  /**
33
35
  * Sets the session ID.
@@ -285,7 +287,12 @@ declare enum ManagerEventTypes {
285
287
  TrackEnd = "trackEnd",
286
288
  TrackError = "trackError",
287
289
  TrackStart = "trackStart",
288
- TrackStuck = "trackStuck"
290
+ TrackStuck = "trackStuck",
291
+ VoiceReceiverDisconnect = "voiceReceiverDisconnect",
292
+ VoiceReceiverConnect = "voiceReceiverConnect",
293
+ VoiceReceiverError = "voiceReceiverError",
294
+ VoiceReceiverStartSpeaking = "voiceReceiverStartSpeaking",
295
+ VoiceReceiverEndSpeaking = "voiceReceiverEndSpeaking"
289
296
  }
290
297
  /**
291
298
  * Track End Reason Enum
@@ -801,6 +808,11 @@ interface ManagerEvents {
801
808
  [ManagerEventTypes.TrackError]: [player: Player, track: Track, payload: TrackExceptionEvent];
802
809
  [ManagerEventTypes.TrackStart]: [player: Player, track: Track, payload: TrackStartEvent];
803
810
  [ManagerEventTypes.TrackStuck]: [player: Player, track: Track, payload: TrackStuckEvent];
811
+ [ManagerEventTypes.VoiceReceiverDisconnect]: [player: Player];
812
+ [ManagerEventTypes.VoiceReceiverConnect]: [player: Player];
813
+ [ManagerEventTypes.VoiceReceiverError]: [player: Player, error: Error];
814
+ [ManagerEventTypes.VoiceReceiverStartSpeaking]: [player: Player, data: unknown];
815
+ [ManagerEventTypes.VoiceReceiverEndSpeaking]: [player: Player, data: unknown];
804
816
  }
805
817
  /**
806
818
  * Voice Packet
@@ -997,6 +1009,8 @@ interface NodeOptions {
997
1009
  apiRequestTimeoutMs?: number;
998
1010
  /** Priority of the node. */
999
1011
  nodePriority?: number;
1012
+ /** Whether the node is a NodeLink. */
1013
+ isNodeLink?: boolean;
1000
1014
  }
1001
1015
  /**
1002
1016
  * NodeOptions interface
@@ -1139,6 +1153,100 @@ interface Lyrics {
1139
1153
  lines: LyricsLine[];
1140
1154
  plugin: object[];
1141
1155
  }
1156
+ /**
1157
+ * NodeLink Get Lyrics Multiple interface
1158
+ */
1159
+ interface NodeLinkGetLyricsMultiple {
1160
+ loadType: "lyricsMultiple";
1161
+ data: NodeLinkGetLyricsData[];
1162
+ }
1163
+ /**
1164
+ * NodeLink Get Lyrics Empty interface
1165
+ */
1166
+ interface NodeLinkGetLyricsEmpty {
1167
+ loadType: "empty";
1168
+ data: Record<never, never>;
1169
+ }
1170
+ /**
1171
+ * NodeLink Get Lyrics Data interface
1172
+ */
1173
+ interface NodeLinkGetLyricsData {
1174
+ name: string;
1175
+ synced: boolean;
1176
+ data: {
1177
+ startTime?: number;
1178
+ endTime?: number;
1179
+ text: string;
1180
+ }[];
1181
+ rtl: boolean;
1182
+ }
1183
+ /**
1184
+ * NodeLink Get Lyrics Single interface
1185
+ */
1186
+ interface NodeLinkGetLyricsSingle {
1187
+ loadType: "lyricsSingle";
1188
+ data: NodeLinkGetLyricsData;
1189
+ }
1190
+ /**
1191
+ * NodeLink Get Lyrics Error interface
1192
+ */
1193
+ interface NodeLinkGetLyricsError {
1194
+ loadType: "error";
1195
+ data: {
1196
+ message: string;
1197
+ severity: Severity;
1198
+ cause: string;
1199
+ trace?: string;
1200
+ };
1201
+ }
1202
+ interface StartSpeakingEventVoiceReceiverData {
1203
+ /**
1204
+ * The user ID of the user who started speaking.
1205
+ */
1206
+ userId: string;
1207
+ /**
1208
+ * The guild ID of the guild where the user started speaking.
1209
+ */
1210
+ guildId: string;
1211
+ }
1212
+ interface EndSpeakingEventVoiceReceiverData {
1213
+ /**
1214
+ * The user ID of the user who stopped speaking.
1215
+ */
1216
+ userId: string;
1217
+ /**
1218
+ * The guild ID of the guild where the user stopped speaking.
1219
+ */
1220
+ guildId: string;
1221
+ /**
1222
+ * The audio data received from the user in base64.
1223
+ */
1224
+ data: string;
1225
+ /**
1226
+ * The type of the audio data. Can be either opus or pcm. Older versions may include ogg/opus.
1227
+ */
1228
+ type: "opus" | "pcm";
1229
+ }
1230
+ /**
1231
+ * Base Voice Receiver Event interface
1232
+ */
1233
+ interface BaseVoiceReceiverEvent {
1234
+ op: "speak";
1235
+ }
1236
+ /**
1237
+ * Start Speaking Event Voice Receiver interface
1238
+ */
1239
+ interface StartSpeakingEventVoiceReceiver extends BaseVoiceReceiverEvent {
1240
+ type: "startSpeakingEvent";
1241
+ data: StartSpeakingEventVoiceReceiverData;
1242
+ }
1243
+ /**
1244
+ * End Speaking Event Voice Receiver interface
1245
+ */
1246
+ interface EndSpeakingEventVoiceReceiver extends BaseVoiceReceiverEvent {
1247
+ type: "endSpeakingEvent";
1248
+ data: EndSpeakingEventVoiceReceiverData;
1249
+ }
1142
1250
  /**
1143
1251
  * PlayerOptions interface
1144
1252
  */
@@ -1179,25 +1287,6 @@ interface EqualizerBand {
1179
1287
  /** The gain amount being -0.25 to 1.00, 0.25 being double. */
1180
1288
  gain: number;
1181
1289
  }
1182
- /**
1183
- * PlayerStore interface
1184
- */
1185
- interface PlayerStore {
1186
- get(guildId: string): Promise<Player | undefined>;
1187
- set(guildId: string, player: Player): Promise<void>;
1188
- delete(guildId: string): Promise<void>;
1189
- keys(): Promise<string[]>;
1190
- has(guildId: string): Promise<boolean>;
1191
- filter(predicate: (player: Player, guildId: string) => boolean | Promise<boolean>): Promise<Map<string, Player>>;
1192
- find(predicate: (player: Player, guildId: string) => boolean | Promise<boolean>): Promise<Player | undefined>;
1193
- map<T>(callback: (player: Player, guildId: string) => T | Promise<T>): Promise<T[]>;
1194
- forEach(callback: (player: Player, guildId: string) => void | Promise<void>): Promise<void>;
1195
- some(predicate: (player: Player, guildId: string) => boolean | Promise<boolean>): Promise<boolean>;
1196
- every(predicate: (player: Player, guildId: string) => boolean | Promise<boolean>): Promise<boolean>;
1197
- size(): Promise<number>;
1198
- clear(): Promise<void>;
1199
- entries(): AsyncIterableIterator<[string, Player]>;
1200
- }
1201
1290
  /**
1202
1291
  * Queue interface
1203
1292
  */
@@ -1270,6 +1359,14 @@ type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExc
1270
1359
  * Load Type Enum type
1271
1360
  */
1272
1361
  type LoadType = keyof typeof LoadTypes;
1362
+ /**
1363
+ * NodeLink Get Lyrics Enum type
1364
+ */
1365
+ type NodeLinkGetLyrics = NodeLinkGetLyricsSingle | NodeLinkGetLyricsMultiple | NodeLinkGetLyricsEmpty | NodeLinkGetLyricsError;
1366
+ /**
1367
+ * Voice Receiver Event Enum type
1368
+ */
1369
+ type VoiceReceiverEvent = StartSpeakingEventVoiceReceiver | EndSpeakingEventVoiceReceiver;
1273
1370
 
1274
1371
  declare class Node$1 {
1275
1372
  manager: Manager;
@@ -1285,11 +1382,14 @@ declare class Node$1 {
1285
1382
  readonly rest: Rest;
1286
1383
  /** Actual Lavalink information of the node. */
1287
1384
  info: LavalinkInfo | null;
1385
+ /** Whether the node is a NodeLink. */
1386
+ isNodeLink: boolean;
1288
1387
  private reconnectTimeout?;
1289
1388
  private reconnectAttempts;
1290
1389
  /**
1291
1390
  * Creates an instance of Node.
1292
- * @param options
1391
+ * @param manager - The manager for the node.
1392
+ * @param options - The options for the node.
1293
1393
  */
1294
1394
  constructor(manager: Manager, options: NodeOptions);
1295
1395
  /** Returns if connected to the Node. */
@@ -1361,6 +1461,12 @@ declare class Node$1 {
1361
1461
  * @emits {nodeDestroy} - Emits a nodeDestroy event if the maximum number of retry attempts is reached.
1362
1462
  */
1363
1463
  private reconnect;
1464
+ /**
1465
+ * Upgrades the node to a NodeLink.
1466
+ *
1467
+ * @param request - The incoming message.
1468
+ */
1469
+ private upgrade;
1364
1470
  /**
1365
1471
  * Handles the "open" event emitted by the WebSocket connection.
1366
1472
  *
@@ -1492,9 +1598,9 @@ declare class Node$1 {
1492
1598
  *
1493
1599
  * @param {Track} track - The track to fetch the lyrics for.
1494
1600
  * @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
1495
- * @returns {Promise<Lyrics>} A promise that resolves with the lyrics data.
1601
+ * @returns {Promise<Lyrics | NodeLinkGetLyrics>} A promise that resolves with the lyrics data.
1496
1602
  */
1497
- getLyrics(track: Track, skipTrackSource?: boolean): Promise<Lyrics>;
1603
+ getLyrics(track: Track, skipTrackSource?: boolean): Promise<Lyrics | NodeLinkGetLyrics>;
1498
1604
  /**
1499
1605
  * Handles the event when a track becomes stuck during playback.
1500
1606
  * Stops the current track and emits a `trackStuck` event.
@@ -1808,6 +1914,11 @@ declare class Manager extends EventEmitter {
1808
1914
  * This is done to prevent stale state files from accumulating on the file system.
1809
1915
  */
1810
1916
  private cleanupInactivePlayers;
1917
+ /**
1918
+ * Clears all player states from the file system.
1919
+ * This is done to prevent stale state files from accumulating on the file system.
1920
+ */
1921
+ private clearAllPlayerStates;
1811
1922
  /**
1812
1923
  * Returns the nodes that has the least load.
1813
1924
  * The load is calculated by dividing the lavalink load by the number of cores.
@@ -1884,6 +1995,12 @@ declare class Player {
1884
1995
  private dynamicLoopInterval;
1885
1996
  dynamicRepeatIntervalMs: number | null;
1886
1997
  private static _manager;
1998
+ /** Should only be used when the node is a NodeLink */
1999
+ protected voiceReceiverWsClient: WebSocket$1 | null;
2000
+ protected isConnectToVoiceReceiver: boolean;
2001
+ protected voiceReceiverReconnectTimeout: NodeJS.Timeout | null;
2002
+ protected voiceReceiverAttempt: number;
2003
+ protected voiceReceiverReconnectTries: number;
1887
2004
  /**
1888
2005
  * Creates a new player, returns one if it already exists.
1889
2006
  * @param options The player options.
@@ -2116,11 +2233,51 @@ declare class Player {
2116
2233
  */
2117
2234
  getCurrentLyrics(skipTrackSource?: boolean): Promise<Lyrics>;
2118
2235
  /**
2119
- * Sleeps for a specified amount of time.
2120
- * @param ms The amount of time to sleep in milliseconds.
2121
- * @returns A promise that resolves after the specified amount of time.
2236
+ * Sets up the voice receiver for the player.
2237
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is set up.
2238
+ * @throws {Error} - If the node is not a NodeLink.
2122
2239
  */
2123
- private sleep;
2240
+ setupVoiceReceiver(): Promise<void>;
2241
+ /**
2242
+ * Removes the voice receiver for the player.
2243
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is removed.
2244
+ * @throws {Error} - If the node is not a NodeLink.
2245
+ */
2246
+ removeVoiceReceiver(): Promise<void>;
2247
+ /**
2248
+ * Closes the voice receiver for the player.
2249
+ * @param {number} code - The code to close the voice receiver with.
2250
+ * @param {string} reason - The reason to close the voice receiver with.
2251
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is closed.
2252
+ */
2253
+ private closeVoiceReceiver;
2254
+ /**
2255
+ * Reconnects the voice receiver for the player.
2256
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is reconnected.
2257
+ */
2258
+ private reconnectVoiceReceiver;
2259
+ /**
2260
+ * Disconnects the voice receiver for the player.
2261
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is disconnected.
2262
+ */
2263
+ private disconnectVoiceReceiver;
2264
+ /**
2265
+ * Opens the voice receiver for the player.
2266
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is opened.
2267
+ */
2268
+ private openVoiceReceiver;
2269
+ /**
2270
+ * Handles a voice receiver message.
2271
+ * @param {string} payload - The payload to handle.
2272
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
2273
+ */
2274
+ private onVoiceReceiverMessage;
2275
+ /**
2276
+ * Handles a voice receiver error.
2277
+ * @param {Error} error - The error to handle.
2278
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver error is handled.
2279
+ */
2280
+ private onVoiceReceiverError;
2124
2281
  }
2125
2282
 
2126
2283
  declare class Filters {
@@ -2604,9 +2761,7 @@ declare abstract class AutoPlayUtils {
2604
2761
  * @returns An array of recommended tracks.
2605
2762
  */
2606
2763
  static getRecommendedTracksFromSource(track: Track, platform: string): Promise<Track[]>;
2607
- static fetchSecretArray(): Promise<Buffer<ArrayBuffer>>;
2608
- static transformSecret(buffer: Buffer): Buffer<ArrayBuffer>;
2609
- static generateTotp(secretBuffer: Buffer): string;
2764
+ static generateTotp(): (string | number)[];
2610
2765
  }
2611
2766
  /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
2612
2767
  declare abstract class Structure {
@@ -2660,4 +2815,4 @@ declare class OceanicManager extends Manager {
2660
2815
  }
2661
2816
 
2662
2817
  export { AutoPlayPlatform, AutoPlayUtils, AvailableFilters, DetritusManager, DiscordJSManager, ErisManager, Filters, LoadTypes, Manager, ManagerEventTypes, Node$1 as Node, OceanicManager, Player, PlayerStateEventTypes, Plugin$1 as Plugin, Queue, Rest, SearchPlatform, SeverityTypes, SponsorBlockSegment, StateStorageType, StateTypes, Structure, TrackEndReasonTypes, TrackPartial, TrackSourceTypes, TrackUtils, UseNodeOptions };
2663
- export type { CPUStats, DiscordPacket, EqualizerBand, Exception, Extendable, FrameStats, IQueue, LavaPlayer, LavalinkInfo, LavalinkResponse, LoadType, Lyrics, LyricsLine, ManagerEvents, ManagerInitOptions, ManagerOptions, MemoryStats, NodeMessage, NodeOptions, NodeStats, Payload, PlayOptions, PlayerEvent, PlayerEventType, PlayerEvents, PlayerOptions, PlayerStateUpdateEvent, PlayerStore, PlayerUpdate, PlayerUpdateVoiceState, PlaylistData, PlaylistInfoData, PlaylistRawData, RedisConfig, SearchQuery, SearchResult, Severity, Sizes, SponsorBlockChapterStarted, SponsorBlockChaptersLoaded, SponsorBlockSegmentEventType, SponsorBlockSegmentEvents, SponsorBlockSegmentSkipped, SponsorBlockSegmentsLoaded, StateStorageOptions, Track, TrackData, TrackDataInfo, TrackEndEvent, TrackEndReason, TrackExceptionEvent, TrackPluginInfo, TrackSourceName, TrackStartEvent, TrackStuckEvent, UseNodeOption, VoicePacket, VoiceServer, VoiceServerUpdate, VoiceState, WebSocketClosedEvent };
2818
+ export type { CPUStats, DiscordPacket, EndSpeakingEventVoiceReceiver, EndSpeakingEventVoiceReceiverData, EqualizerBand, Exception, Extendable, FrameStats, IQueue, LavaPlayer, LavalinkInfo, LavalinkResponse, LoadType, Lyrics, LyricsLine, ManagerEvents, ManagerInitOptions, ManagerOptions, MemoryStats, NodeLinkGetLyrics, NodeLinkGetLyricsEmpty, NodeLinkGetLyricsError, NodeLinkGetLyricsMultiple, NodeLinkGetLyricsSingle, NodeMessage, NodeOptions, NodeStats, Payload, PlayOptions, PlayerEvent, PlayerEventType, PlayerEvents, PlayerOptions, PlayerStateUpdateEvent, PlayerUpdate, PlayerUpdateVoiceState, PlaylistData, PlaylistInfoData, PlaylistRawData, RedisConfig, SearchQuery, SearchResult, Severity, Sizes, SponsorBlockChapterStarted, SponsorBlockChaptersLoaded, SponsorBlockSegmentEventType, SponsorBlockSegmentEvents, SponsorBlockSegmentSkipped, SponsorBlockSegmentsLoaded, StartSpeakingEventVoiceReceiver, StartSpeakingEventVoiceReceiverData, StateStorageOptions, Track, TrackData, TrackDataInfo, TrackEndEvent, TrackEndReason, TrackExceptionEvent, TrackPluginInfo, TrackSourceName, TrackStartEvent, TrackStuckEvent, UseNodeOption, VoicePacket, VoiceReceiverEvent, VoiceServer, VoiceServerUpdate, VoiceState, WebSocketClosedEvent };
@@ -163,6 +163,11 @@ var ManagerEventTypes;
163
163
  ManagerEventTypes["TrackError"] = "trackError";
164
164
  ManagerEventTypes["TrackStart"] = "trackStart";
165
165
  ManagerEventTypes["TrackStuck"] = "trackStuck";
166
+ ManagerEventTypes["VoiceReceiverDisconnect"] = "voiceReceiverDisconnect";
167
+ ManagerEventTypes["VoiceReceiverConnect"] = "voiceReceiverConnect";
168
+ ManagerEventTypes["VoiceReceiverError"] = "voiceReceiverError";
169
+ ManagerEventTypes["VoiceReceiverStartSpeaking"] = "voiceReceiverStartSpeaking";
170
+ ManagerEventTypes["VoiceReceiverEndSpeaking"] = "voiceReceiverEndSpeaking";
166
171
  })(ManagerEventTypes || (exports.ManagerEventTypes = ManagerEventTypes = {}));
167
172
  /**
168
173
  * Track End Reason Enum
@@ -791,6 +791,9 @@ class Manager extends events_1.EventEmitter {
791
791
  async handleShutdown() {
792
792
  console.warn("\x1b[31m%s\x1b[0m", "MAGMASTREAM WARNING: Shutting down! Please wait, saving active players...");
793
793
  try {
794
+ if (this.options.stateStorage.type === Enums_1.StateStorageType.Collection) {
795
+ await this.clearAllPlayerStates();
796
+ }
794
797
  const savePromises = Array.from(this.players.keys()).map(async (guildId) => {
795
798
  try {
796
799
  await this.savePlayerState(guildId);
@@ -925,6 +928,7 @@ class Manager extends events_1.EventEmitter {
925
928
  guildId: player.guildId,
926
929
  data: { voice: { token, endpoint, sessionId } },
927
930
  });
931
+ this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice server for player ${player.guildId} with token ${token} and endpoint ${endpoint} and sessionId ${sessionId}`);
928
932
  return;
929
933
  }
930
934
  /**
@@ -935,6 +939,7 @@ class Manager extends events_1.EventEmitter {
935
939
  * @emits {playerDisconnect} - Emits a player disconnect event if the channel ID is null.
936
940
  */
937
941
  async handleVoiceStateUpdate(player, update) {
942
+ this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice state for player ${player.guildId} with channel id ${update.channel_id} and session id ${update.session_id}`);
938
943
  if (update.channel_id) {
939
944
  if (player.voiceChannelId !== update.channel_id) {
940
945
  this.emit(Enums_1.ManagerEventTypes.PlayerMove, player, player.voiceChannelId, update.channel_id);
@@ -946,7 +951,7 @@ class Manager extends events_1.EventEmitter {
946
951
  this.emit(Enums_1.ManagerEventTypes.PlayerDisconnect, player, player.voiceChannelId);
947
952
  player.voiceChannelId = null;
948
953
  player.voiceState = Object.assign({});
949
- await player.destroy();
954
+ await player.pause(true);
950
955
  return;
951
956
  }
952
957
  /**
@@ -1052,6 +1057,64 @@ class Manager extends events_1.EventEmitter {
1052
1057
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error cleaning up inactive players: ${error}`);
1053
1058
  }
1054
1059
  }
1060
+ /**
1061
+ * Clears all player states from the file system.
1062
+ * This is done to prevent stale state files from accumulating on the file system.
1063
+ */
1064
+ async clearAllPlayerStates() {
1065
+ switch (this.options.stateStorage.type) {
1066
+ case Enums_1.StateStorageType.Collection: {
1067
+ const configDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
1068
+ try {
1069
+ // Check if the directory exists, and create it if it doesn't
1070
+ await promises_1.default.access(configDir).catch(async () => {
1071
+ await promises_1.default.mkdir(configDir, { recursive: true });
1072
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Created directory: ${configDir}`);
1073
+ });
1074
+ const files = await promises_1.default.readdir(configDir);
1075
+ await Promise.all(files.map((file) => promises_1.default.unlink(path_1.default.join(configDir, file)).catch((err) => console.warn(`Failed to delete file ${file}:`, err))));
1076
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared all player state files in ${configDir}`);
1077
+ }
1078
+ catch (err) {
1079
+ console.error("Error clearing player state files:", err);
1080
+ }
1081
+ break;
1082
+ }
1083
+ case Enums_1.StateStorageType.Redis: {
1084
+ const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
1085
+ ? this.options.stateStorage.redisConfig.prefix
1086
+ : this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
1087
+ const pattern = `${prefix}playerstore:*`;
1088
+ try {
1089
+ const stream = this.redis.scanStream({
1090
+ match: pattern,
1091
+ count: 100,
1092
+ });
1093
+ let totalDeleted = 0;
1094
+ stream.on("data", async (keys) => {
1095
+ if (keys.length) {
1096
+ const pipeline = this.redis.pipeline();
1097
+ keys.forEach((key) => pipeline.unlink(key));
1098
+ await pipeline.exec();
1099
+ totalDeleted += keys.length;
1100
+ }
1101
+ });
1102
+ stream.on("end", () => {
1103
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared ${totalDeleted} Redis player state keys (pattern: ${pattern})`);
1104
+ });
1105
+ stream.on("error", (err) => {
1106
+ console.error("Error during Redis SCAN stream:", err);
1107
+ });
1108
+ }
1109
+ catch (err) {
1110
+ console.error("Failed to clear Redis player state keys:", err);
1111
+ }
1112
+ break;
1113
+ }
1114
+ default:
1115
+ console.warn("[MANAGER] No valid stateStorage.type set, skipping state clearing.");
1116
+ }
1117
+ }
1055
1118
  /**
1056
1119
  * Returns the nodes that has the least load.
1057
1120
  * The load is calculated by dividing the lavalink load by the number of cores.
@@ -31,11 +31,14 @@ class Node {
31
31
  rest;
32
32
  /** Actual Lavalink information of the node. */
33
33
  info = null;
34
+ /** Whether the node is a NodeLink. */
35
+ isNodeLink = false;
34
36
  reconnectTimeout;
35
37
  reconnectAttempts = 1;
36
38
  /**
37
39
  * Creates an instance of Node.
38
- * @param options
40
+ * @param manager - The manager for the node.
41
+ * @param options - The options for the node.
39
42
  */
40
43
  constructor(manager, options) {
41
44
  this.manager = manager;
@@ -185,6 +188,7 @@ class Node {
185
188
  this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
186
189
  this.socket.on("open", this.open.bind(this));
187
190
  this.socket.on("close", this.close.bind(this));
191
+ this.socket.on("upgrade", (request) => this.upgrade(request));
188
192
  this.socket.on("message", this.message.bind(this));
189
193
  this.socket.on("error", this.error.bind(this));
190
194
  const debugInfo = {
@@ -287,6 +291,14 @@ class Node {
287
291
  this.reconnectAttempts++;
288
292
  }, this.options.retryDelayMs);
289
293
  }
294
+ /**
295
+ * Upgrades the node to a NodeLink.
296
+ *
297
+ * @param request - The incoming message.
298
+ */
299
+ upgrade(request) {
300
+ this.isNodeLink = this.options.isNodeLink ?? Boolean(request.headers.isnodelink) ?? false;
301
+ }
290
302
  /**
291
303
  * Handles the "open" event emitted by the WebSocket connection.
292
304
  *
@@ -727,13 +739,14 @@ class Node {
727
739
  *
728
740
  * @param {Track} track - The track to fetch the lyrics for.
729
741
  * @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
730
- * @returns {Promise<Lyrics>} A promise that resolves with the lyrics data.
742
+ * @returns {Promise<Lyrics | NodeLinkGetLyrics>} A promise that resolves with the lyrics data.
731
743
  */
732
744
  async getLyrics(track, skipTrackSource = false) {
745
+ if (this.isNodeLink) {
746
+ return (await this.rest.get(`/v4/lyrics?track=${encodeURIComponent(track.track)}&skipTrackSource=${skipTrackSource}`));
747
+ }
733
748
  if (!this.info.plugins.some((plugin) => plugin.name === "lavalyrics-plugin"))
734
749
  throw new RangeError(`there is no lavalyrics-plugin available in the lavalink node: ${this.options.identifier}`);
735
- // Make a GET request to the Lavalink node to fetch the lyrics
736
- // The request includes the track URL and the skipTrackSource parameter
737
750
  return ((await this.rest.get(`/v4/lyrics?track=${encodeURIComponent(track.track)}&skipTrackSource=${skipTrackSource}`)) || {
738
751
  source: null,
739
752
  provider: null,
@@ -10,6 +10,7 @@ const playerCheck_1 = tslib_1.__importDefault(require("../utils/playerCheck"));
10
10
  const RedisQueue_1 = require("./RedisQueue");
11
11
  // import { IQueue, Lyrics, PlayerOptions, PlayerUpdateVoiceState, PlayOptions, SearchQuery, SearchResult, Track, VoiceState } from "./Types";
12
12
  const Enums_1 = require("./Enums");
13
+ const ws_1 = require("ws");
13
14
  class Player {
14
15
  options;
15
16
  /** The Queue for the Player. */
@@ -58,6 +59,12 @@ class Player {
58
59
  dynamicLoopInterval = null;
59
60
  dynamicRepeatIntervalMs = null;
60
61
  static _manager;
62
+ /** Should only be used when the node is a NodeLink */
63
+ voiceReceiverWsClient;
64
+ isConnectToVoiceReceiver;
65
+ voiceReceiverReconnectTimeout;
66
+ voiceReceiverAttempt;
67
+ voiceReceiverReconnectTries;
61
68
  /**
62
69
  * Creates a new player, returns one if it already exists.
63
70
  * @param options The player options.
@@ -911,12 +918,132 @@ class Player {
911
918
  return result;
912
919
  }
913
920
  /**
914
- * Sleeps for a specified amount of time.
915
- * @param ms The amount of time to sleep in milliseconds.
916
- * @returns A promise that resolves after the specified amount of time.
921
+ * Sets up the voice receiver for the player.
922
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is set up.
923
+ * @throws {Error} - If the node is not a NodeLink.
917
924
  */
918
- sleep(ms) {
919
- return new Promise((resolve) => setTimeout(resolve, ms));
925
+ async setupVoiceReceiver() {
926
+ if (!this.node.isNodeLink)
927
+ throw new Error("This function is only available for NodeLinks");
928
+ if (this.voiceReceiverWsClient)
929
+ await this.removeVoiceReceiver();
930
+ const headers = {
931
+ Authorization: this.node.options.password,
932
+ "User-Id": this.manager.options.clientId,
933
+ "Guild-Id": this.guildId,
934
+ "Client-Name": this.manager.options.clientName,
935
+ };
936
+ const { host, useSSL, port } = this.node.options;
937
+ this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/connection/data`, { headers });
938
+ this.voiceReceiverWsClient.on("open", () => this.openVoiceReceiver());
939
+ this.voiceReceiverWsClient.on("error", (err) => this.onVoiceReceiverError(err));
940
+ this.voiceReceiverWsClient.on("message", (data) => this.onVoiceReceiverMessage(data.toString()));
941
+ this.voiceReceiverWsClient.on("close", (code, reason) => this.closeVoiceReceiver(code, reason.toString()));
942
+ }
943
+ /**
944
+ * Removes the voice receiver for the player.
945
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is removed.
946
+ * @throws {Error} - If the node is not a NodeLink.
947
+ */
948
+ async removeVoiceReceiver() {
949
+ if (!this.node.isNodeLink)
950
+ throw new Error("This function is only available for NodeLinks");
951
+ if (this.voiceReceiverWsClient) {
952
+ this.voiceReceiverWsClient.close(1000, "destroy");
953
+ this.voiceReceiverWsClient.removeAllListeners();
954
+ this.voiceReceiverWsClient = null;
955
+ }
956
+ this.isConnectToVoiceReceiver = false;
957
+ }
958
+ /**
959
+ * Closes the voice receiver for the player.
960
+ * @param {number} code - The code to close the voice receiver with.
961
+ * @param {string} reason - The reason to close the voice receiver with.
962
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is closed.
963
+ */
964
+ async closeVoiceReceiver(code, reason) {
965
+ await this.disconnectVoiceReceiver();
966
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Closed voice receiver for player ${this.guildId} with code ${code} and reason ${reason}`);
967
+ if (code !== 1000)
968
+ await this.reconnectVoiceReceiver();
969
+ }
970
+ /**
971
+ * Reconnects the voice receiver for the player.
972
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is reconnected.
973
+ */
974
+ async reconnectVoiceReceiver() {
975
+ this.voiceReceiverReconnectTimeout = setTimeout(async () => {
976
+ if (this.voiceReceiverAttempt > this.voiceReceiverReconnectTries)
977
+ throw new Error("Failed to reconnect to voice receiver");
978
+ this.voiceReceiverWsClient?.removeAllListeners();
979
+ this.voiceReceiverWsClient = null;
980
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Reconnecting to voice receiver for player ${this.guildId}`);
981
+ await this.setupVoiceReceiver();
982
+ this.voiceReceiverAttempt++;
983
+ }, this.node.options.retryDelayMs);
984
+ }
985
+ /**
986
+ * Disconnects the voice receiver for the player.
987
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is disconnected.
988
+ */
989
+ async disconnectVoiceReceiver() {
990
+ if (!this.isConnectToVoiceReceiver)
991
+ return;
992
+ this.voiceReceiverWsClient?.close(1000, "destroy");
993
+ this.voiceReceiverWsClient?.removeAllListeners();
994
+ this.voiceReceiverWsClient = null;
995
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Disconnected from voice receiver for player ${this.guildId}`);
996
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverDisconnect, this);
997
+ }
998
+ /**
999
+ * Opens the voice receiver for the player.
1000
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver is opened.
1001
+ */
1002
+ async openVoiceReceiver() {
1003
+ if (this.voiceReceiverReconnectTimeout)
1004
+ clearTimeout(this.voiceReceiverReconnectTimeout);
1005
+ this.voiceReceiverReconnectTimeout = null;
1006
+ this.isConnectToVoiceReceiver = true;
1007
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Opened voice receiver for player ${this.guildId}`);
1008
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverConnect, this);
1009
+ }
1010
+ /**
1011
+ * Handles a voice receiver message.
1012
+ * @param {string} payload - The payload to handle.
1013
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
1014
+ */
1015
+ async onVoiceReceiverMessage(payload) {
1016
+ const packet = JSON.parse(payload);
1017
+ if (!packet?.op)
1018
+ return;
1019
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${JSON.stringify(payload)}`);
1020
+ switch (packet.type) {
1021
+ case "startSpeakingEvent": {
1022
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, packet.data);
1023
+ break;
1024
+ }
1025
+ case "endSpeakingEvent": {
1026
+ const data = {
1027
+ ...packet.data,
1028
+ data: Buffer.from(packet.data.data, "base64"),
1029
+ };
1030
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverEndSpeaking, this, data);
1031
+ break;
1032
+ }
1033
+ default: {
1034
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${JSON.stringify(payload)}`);
1035
+ break;
1036
+ }
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Handles a voice receiver error.
1041
+ * @param {Error} error - The error to handle.
1042
+ * @returns {Promise<void>} - A promise that resolves when the voice receiver error is handled.
1043
+ */
1044
+ async onVoiceReceiverError(error) {
1045
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver error for player ${this.guildId}: ${error.message}`);
1046
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverError, this, error);
920
1047
  }
921
1048
  }
922
1049
  exports.Player = Player;
@@ -16,12 +16,15 @@ class Rest {
16
16
  url;
17
17
  /** The Manager instance. */
18
18
  manager;
19
+ /** Whether the node is a NodeLink. */
20
+ isNodeLink = false;
19
21
  constructor(node, manager) {
20
22
  this.node = node;
21
23
  this.url = `http${node.options.useSSL ? "s" : ""}://${node.options.host}:${node.options.port}`;
22
24
  this.sessionId = node.sessionId;
23
25
  this.password = node.options.password;
24
26
  this.manager = manager;
27
+ this.isNodeLink = node.isNodeLink;
25
28
  }
26
29
  /**
27
30
  * Sets the session ID.
@@ -5,7 +5,6 @@ const tslib_1 = require("tslib");
5
5
  const axios_1 = tslib_1.__importDefault(require("axios"));
6
6
  const jsdom_1 = require("jsdom");
7
7
  const crypto_1 = tslib_1.__importDefault(require("crypto"));
8
- const cheerio_1 = tslib_1.__importDefault(require("cheerio"));
9
8
  const Enums_1 = require("./Enums");
10
9
  /** @hidden */
11
10
  const SIZES = ["0", "1", "2", "3", "default", "mqdefault", "hqdefault", "maxresdefault"];
@@ -278,31 +277,17 @@ class AutoPlayUtils {
278
277
  }
279
278
  track = res.tracks[0];
280
279
  }
281
- const periodMs = 30000;
282
- async function getSpotifyTotpParams() {
283
- try {
284
- const secretBuffer = await AutoPlayUtils.fetchSecretArray();
285
- const transformedSecret = AutoPlayUtils.transformSecret(secretBuffer);
286
- const totp = AutoPlayUtils.generateTotp(transformedSecret);
287
- const timestamp = Math.floor(Date.now() / periodMs) * periodMs;
288
- const params = {
289
- reason: "transport",
290
- productType: "embed",
291
- totp,
292
- totpVer: 5,
293
- ts: timestamp,
294
- };
295
- return params;
296
- }
297
- catch (error) {
298
- console.error("Failed to generate Spotify TOTP params:", error);
299
- throw error;
300
- }
301
- }
280
+ const [totp, timestamp] = this.generateTotp();
281
+ const params = {
282
+ reason: "init",
283
+ productType: "web-player",
284
+ totp: totp,
285
+ totpVer: 5,
286
+ ts: timestamp,
287
+ };
302
288
  let body;
303
289
  try {
304
- const params = await getSpotifyTotpParams();
305
- const response = await axios_1.default.get("https://open.spotify.com/get_access_token", {
290
+ const response = await axios_1.default.get("https://open.spotify.com/api/token", {
306
291
  params,
307
292
  headers: {
308
293
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.178 Spotify/1.2.65.255 Safari/537.36",
@@ -310,13 +295,13 @@ class AutoPlayUtils {
310
295
  Referer: "https://open.spotify.com/",
311
296
  Origin: "https://open.spotify.com",
312
297
  "Accept-Language": "en",
313
- "Content-Type": "application/json",
314
298
  },
315
299
  });
316
300
  body = response.data;
317
301
  }
318
302
  catch (error) {
319
- console.error("[AutoPlay] Failed to get spotify access token:", error.response?.status);
303
+ const status = error.response?.status ?? "No response";
304
+ console.error("[AutoPlay] Failed to get spotify access token:", status);
320
305
  return [];
321
306
  }
322
307
  let json;
@@ -673,51 +658,20 @@ class AutoPlayUtils {
673
658
  return [];
674
659
  }
675
660
  }
676
- static async fetchSecretArray() {
677
- // Step 1: Get Spotify homepage HTML
678
- const homepageRes = await axios_1.default.get("https://open.spotify.com/");
679
- const $ = cheerio_1.default.load(homepageRes.data);
680
- // Step 2: Find script URL with "mobile-web-player" but not "vendor"
681
- const scriptUrl = $("script[src]")
682
- .map((_, el) => $(el).attr("src"))
683
- .get()
684
- .find((src) => src.includes("mobile-web-player") && !src.includes("vendor"));
685
- if (!scriptUrl)
686
- throw new Error("Secret script not found");
687
- // Full URL fix (if needed)
688
- const fullScriptUrl = scriptUrl.startsWith("http") ? scriptUrl : "https://open.spotify.com" + scriptUrl;
689
- // Step 3: Fetch the script content
690
- const scriptRes = await axios_1.default.get(fullScriptUrl);
691
- const scriptContent = scriptRes.data;
692
- // Step 4: Extract secret array using regex
693
- const secretPattern = /\(\[(\d+(?:,\d+)+)]\)/;
694
- const match = secretPattern.exec(scriptContent);
695
- if (!match)
696
- throw new Error("Secret array not found in script");
697
- // Parse the secret array into bytes
698
- const secretArray = match[1].split(",").map((n) => parseInt(n.trim(), 10));
699
- return Buffer.from(secretArray);
700
- }
701
- static transformSecret(buffer) {
702
- const transformed = Buffer.alloc(buffer.length);
703
- for (let i = 0; i < buffer.length; i++) {
704
- transformed[i] = buffer[i] ^ ((i % 33) + 9);
705
- }
706
- return transformed;
707
- }
708
- static generateTotp(secretBuffer) {
709
- const period = 30; // seconds
710
- const digits = 6;
711
- const counter = Math.floor(Date.now() / 1000 / period);
661
+ static generateTotp() {
662
+ const TOTP_SECRET = new Uint8Array([
663
+ 53, 53, 48, 55, 49, 52, 53, 56, 53, 51, 52, 56, 55, 52, 57, 57, 53, 57, 50, 50, 52, 56, 54, 51, 48, 51, 50, 57, 51, 52, 55,
664
+ ]);
665
+ const hmac = crypto_1.default.createHmac("sha1", TOTP_SECRET);
666
+ const counter = Math.floor(Date.now() / 30000);
712
667
  const counterBuffer = Buffer.alloc(8);
713
668
  counterBuffer.writeBigInt64BE(BigInt(counter));
714
- const hmac = crypto_1.default.createHmac("sha1", secretBuffer);
715
669
  hmac.update(counterBuffer);
716
670
  const hmacResult = hmac.digest();
717
- const offset = hmacResult[hmacResult.length - 1] & 0x0f;
718
- const binary = ((hmacResult[offset] & 0x7f) << 24) | ((hmacResult[offset + 1] & 0xff) << 16) | ((hmacResult[offset + 2] & 0xff) << 8) | (hmacResult[offset + 3] & 0xff);
719
- const otp = binary % 10 ** digits;
720
- return otp.toString().padStart(digits, "0");
671
+ const offset = hmacResult[hmacResult.length - 1] & 15;
672
+ const truncatedValue = ((hmacResult[offset] & 127) << 24) | ((hmacResult[offset + 1] & 255) << 16) | ((hmacResult[offset + 2] & 255) << 8) | (hmacResult[offset + 3] & 255);
673
+ const totp = (truncatedValue % 1000000).toString().padStart(6, "0");
674
+ return [totp, counter * 30000];
721
675
  }
722
676
  }
723
677
  exports.AutoPlayUtils = AutoPlayUtils;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.9.0-dev.30",
3
+ "version": "2.9.0-dev.32",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,77 +0,0 @@
1
- "use strict";
2
- // THIS WILL BE REMOVED IF YOU DONT FIND A USE FOR IT.
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.CollectionPlayerStore = void 0;
5
- const collection_1 = require("@discordjs/collection");
6
- class CollectionPlayerStore {
7
- store = new collection_1.Collection();
8
- async get(guildId) {
9
- return this.store.get(guildId);
10
- }
11
- async set(guildId, player) {
12
- this.store.set(guildId, player);
13
- }
14
- async delete(guildId) {
15
- this.store.delete(guildId);
16
- }
17
- async keys() {
18
- return [...this.store.keys()];
19
- }
20
- async has(guildId) {
21
- return this.store.has(guildId);
22
- }
23
- async filter(predicate) {
24
- const result = new Map();
25
- for (const [guildId, player] of this.store.entries()) {
26
- if (await predicate(player, guildId)) {
27
- result.set(guildId, player);
28
- }
29
- }
30
- return result;
31
- }
32
- async find(predicate) {
33
- for (const [guildId, player] of this.store.entries()) {
34
- if (await predicate(player, guildId))
35
- return player;
36
- }
37
- return undefined;
38
- }
39
- async map(callback) {
40
- const results = [];
41
- for (const [guildId, player] of this.store.entries()) {
42
- results.push(await callback(player, guildId));
43
- }
44
- return results;
45
- }
46
- async forEach(callback) {
47
- for (const [guildId, player] of this.store.entries()) {
48
- await callback(player, guildId);
49
- }
50
- }
51
- async some(predicate) {
52
- for (const [guildId, player] of this.store.entries()) {
53
- if (await predicate(player, guildId))
54
- return true;
55
- }
56
- return false;
57
- }
58
- async every(predicate) {
59
- for (const [guildId, player] of this.store.entries()) {
60
- if (!(await predicate(player, guildId)))
61
- return false;
62
- }
63
- return true;
64
- }
65
- async size() {
66
- return this.store.size;
67
- }
68
- async clear() {
69
- this.store.clear();
70
- }
71
- async *entries() {
72
- for (const entry of this.store.entries()) {
73
- yield entry;
74
- }
75
- }
76
- }
77
- exports.CollectionPlayerStore = CollectionPlayerStore;
@@ -1,156 +0,0 @@
1
- "use strict";
2
- // THIS WILL BE REMOVED IF YOU DONT FIND A USE FOR IT.
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.RedisPlayerStore = void 0;
5
- class RedisPlayerStore {
6
- redis;
7
- manager;
8
- prefix;
9
- constructor(redis, manager, prefix = "magmastream:") {
10
- this.redis = redis;
11
- this.manager = manager;
12
- this.prefix = prefix;
13
- }
14
- getKey(guildId) {
15
- return `${this.prefix}player:${guildId}`;
16
- }
17
- async get(guildId) {
18
- const raw = await this.redis.get(this.getKey(guildId));
19
- if (!raw)
20
- return undefined;
21
- return JSON.parse(raw);
22
- }
23
- async set(guildId, player) {
24
- const serialized = this.manager.serializePlayer(player);
25
- await this.redis.set(this.getKey(guildId), JSON.stringify(serialized));
26
- }
27
- async delete(guildId) {
28
- await this.redis.del(this.getKey(guildId));
29
- }
30
- async keys() {
31
- const keys = await this.redis.keys(`${this.prefix}player:*`);
32
- return keys.map((key) => key.replace(`${this.prefix}player:`, ""));
33
- }
34
- async has(guildId) {
35
- return (await this.redis.exists(this.getKey(guildId))) === 1;
36
- }
37
- async filter(predicate) {
38
- const keys = await this.keys();
39
- const pipeline = this.redis.pipeline();
40
- for (const guildId of keys) {
41
- pipeline.get(this.getKey(guildId));
42
- }
43
- const results = await pipeline.exec();
44
- const result = new Map();
45
- for (let i = 0; i < results.length; i++) {
46
- const [err, raw] = results[i];
47
- if (err || typeof raw !== "string")
48
- continue;
49
- const guildId = keys[i];
50
- const player = JSON.parse(raw);
51
- if (await predicate(player, guildId)) {
52
- result.set(guildId, player);
53
- }
54
- }
55
- return result;
56
- }
57
- async find(predicate) {
58
- for (const guildId of await this.keys()) {
59
- const raw = await this.redis.get(this.getKey(guildId));
60
- if (!raw)
61
- continue;
62
- const parsed = JSON.parse(raw);
63
- if (await predicate(parsed, guildId))
64
- return parsed;
65
- }
66
- return undefined;
67
- }
68
- async map(callback) {
69
- const keys = await this.keys();
70
- if (!keys.length)
71
- return [];
72
- const pipeline = this.redis.pipeline();
73
- for (const guildId of keys) {
74
- pipeline.get(this.getKey(guildId));
75
- }
76
- const results = await pipeline.exec();
77
- const output = [];
78
- for (let i = 0; i < results.length; i++) {
79
- const [err, raw] = results[i];
80
- if (err || typeof raw !== "string")
81
- continue;
82
- const guildId = keys[i];
83
- const player = JSON.parse(raw);
84
- output.push(await callback(player, guildId));
85
- }
86
- return output;
87
- }
88
- async forEach(callback) {
89
- for (const guildId of await this.keys()) {
90
- const raw = await this.redis.get(this.getKey(guildId));
91
- if (!raw)
92
- continue;
93
- const parsed = JSON.parse(raw);
94
- await callback(parsed, guildId);
95
- }
96
- }
97
- async some(predicate) {
98
- const keys = await this.keys();
99
- if (!keys.length)
100
- return false;
101
- const pipeline = this.redis.pipeline();
102
- for (const guildId of keys) {
103
- pipeline.get(this.getKey(guildId));
104
- }
105
- const results = await pipeline.exec();
106
- for (let i = 0; i < results.length; i++) {
107
- const [err, raw] = results[i];
108
- if (err || typeof raw !== "string")
109
- continue;
110
- const guildId = keys[i];
111
- const player = JSON.parse(raw);
112
- if (await predicate(player, guildId))
113
- return true;
114
- }
115
- return false;
116
- }
117
- async every(predicate) {
118
- const keys = await this.keys();
119
- if (!keys.length)
120
- return true;
121
- const pipeline = this.redis.pipeline();
122
- for (const guildId of keys) {
123
- pipeline.get(this.getKey(guildId));
124
- }
125
- const results = await pipeline.exec();
126
- for (let i = 0; i < results.length; i++) {
127
- const [err, raw] = results[i];
128
- if (err || typeof raw !== "string")
129
- continue;
130
- const guildId = keys[i];
131
- const player = JSON.parse(raw);
132
- if (!(await predicate(player, guildId)))
133
- return false;
134
- }
135
- return true;
136
- }
137
- async size() {
138
- const keys = await this.keys();
139
- return keys.length;
140
- }
141
- async clear() {
142
- const keys = await this.redis.keys(`${this.prefix}player:*`);
143
- if (keys.length) {
144
- await this.redis.del(...keys);
145
- }
146
- }
147
- async *entries() {
148
- for (const guildId of await this.keys()) {
149
- const raw = await this.redis.get(this.getKey(guildId));
150
- if (!raw)
151
- continue;
152
- yield [guildId, JSON.parse(raw)];
153
- }
154
- }
155
- }
156
- exports.RedisPlayerStore = RedisPlayerStore;