magmastream 2.6.0-beta.2 → 2.6.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,11 @@
1
- <center><img src="https://capsule-render.vercel.app/api?type=waving&color=gradient&height=200&section=header&text=Magmastream&fontSize=80&fontAlignY=35&animation=twinkling&fontColor=gradient" /></center>
2
- <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
3
- [![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)
1
+ <p align="center">
2
+ <img src="https://capsule-render.vercel.app/api?type=waving&color=gradient&height=200&section=header&text=Magmastream&fontSize=80&fontAlignY=35&animation=twinkling&fontColor=gradient" />
3
+ </p>
4
+
5
+ <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
6
+ [![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-)
4
7
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
5
8
 
6
- [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/Magmastream-NPM/magmastream/blob/main/LICENSE)
7
- [![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](https://github.com/Magmastream-NPM/magmastream/pulls)
8
- ![GitHub Stars](https://img.shields.io/github/stars/Magmastream-NPM/magmastream.svg)
9
- [![Issues](https://img.shields.io/github/issues/Magmastream-NPM/magmastream.svg)](https://github.com/Magmastream-NPM/magmastream/issues)
10
- [![Pull Requests](https://img.shields.io/github/issues-pr/Magmastream-NPM/magmastream.svg)](https://github.com/Magmastream-NPM/magmastream/pulls)
11
- [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/Magmastream-NPM)
12
- [![npm](https://img.shields.io/npm/dw/magmastream)](https://www.npmjs.com/package/magmastream)
13
9
 
14
10
  **Magmastream** a lavalink wrapper for NodeJs. Magmastream only supports lavalink version 4.
15
11
 
@@ -55,12 +51,12 @@ If you want to add your own bot create a pull request with your bot added. Pleas
55
51
 
56
52
  Special thanks to the all the contributors who have helped make Magmastream even better!
57
53
 
58
- <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
59
- <!-- prettier-ignore-start -->
60
- <!-- markdownlint-disable -->
61
- <!-- markdownlint-restore -->
62
- <!-- prettier-ignore-end -->
63
-
54
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
55
+ <!-- prettier-ignore-start -->
56
+ <!-- markdownlint-disable -->
57
+ <!-- markdownlint-restore -->
58
+ <!-- prettier-ignore-end -->
59
+
64
60
  <!-- ALL-CONTRIBUTORS-LIST:END -->
65
61
 
66
62
  ## Credits
package/dist/index.d.ts CHANGED
@@ -133,6 +133,11 @@ declare class Queue extends Array<Track | UnresolvedTrack> {
133
133
  current: Track | UnresolvedTrack | null;
134
134
  /** The previous track */
135
135
  previous: Track | UnresolvedTrack | null;
136
+ /** The Manager instance. */
137
+ manager: Manager;
138
+ /** The guild property. */
139
+ guild: string;
140
+ constructor(guild: string, manager: Manager);
136
141
  /**
137
142
  * Adds a track to the queue.
138
143
  * @param track
@@ -224,7 +229,7 @@ declare class Player {
224
229
  * @param query
225
230
  * @param requester
226
231
  */
227
- search(query: string | SearchQuery, requester?: User | ClientUser): Promise<SearchResult>;
232
+ search<T = User | ClientUser>(query: string | SearchQuery, requester?: T): Promise<SearchResult>;
228
233
  /** Connect to the voice channel. */
229
234
  connect(): this;
230
235
  /** Disconnect from the voice channel. */
@@ -242,7 +247,7 @@ declare class Player {
242
247
  */
243
248
  setTextChannel(channel: string): this;
244
249
  /** Sets the now playing message. */
245
- setNowPlayingMessage(message: Message): Message;
250
+ setNowPlayingMessage<T = Message>(message: T): Message;
246
251
  /** Plays the next track. */
247
252
  play(): Promise<void>;
248
253
  /**
@@ -272,7 +277,7 @@ declare class Player {
272
277
  * @param track
273
278
  * @param requester
274
279
  */
275
- getRecommended(track: Track, requester?: User | ClientUser): Promise<Track[]>;
280
+ getRecommended<T = User | ClientUser>(track: Track, requester?: T): Promise<Track[]>;
276
281
  /**
277
282
  * Sets the player volume.
278
283
  * @param volume
@@ -367,7 +372,7 @@ interface Track {
367
372
  /** The thumbnail of the track or null if it's a unsupported source. */
368
373
  readonly thumbnail: string | null;
369
374
  /** The user that requested the track. */
370
- readonly requester: User | ClientUser | null;
375
+ readonly requester?: User | ClientUser;
371
376
  /** Displays the track thumbnail with optional size or null if it's a unsupported source. */
372
377
  displayThumbnail(size?: Sizes): string;
373
378
  /** Additional track info provided by plugins. */
@@ -431,6 +436,8 @@ declare class Rest {
431
436
  updatePlayer(options: playOptions): Promise<unknown>;
432
437
  /** Sends a DELETE request to the server to destroy the player. */
433
438
  destroyPlayer(guildId: string): Promise<unknown>;
439
+ /** Updates the session status for resuming. */
440
+ updateSession(resuming: boolean, timeout: number): Promise<unknown>;
434
441
  private request;
435
442
  get(endpoint: string): Promise<unknown>;
436
443
  patch(endpoint: string, body: unknown): Promise<unknown>;
@@ -497,6 +504,12 @@ declare class Node {
497
504
  * @param options
498
505
  */
499
506
  constructor(options: NodeOptions);
507
+ /** Creates the sessionIds.json file if it doesn't exist. */
508
+ createSessionIdsFile(): void;
509
+ /** Loads session IDs from the sessionIds.json file. */
510
+ loadSessionIds(): void;
511
+ /** Updates the session ID in the sessionIds.json file. */
512
+ updateSessionId(): void;
500
513
  /** Connects to the Node. */
501
514
  connect(): void;
502
515
  /** Destroys the Node and all players connected with it. */
@@ -665,13 +678,13 @@ declare abstract class TrackUtils {
665
678
  * @param data
666
679
  * @param requester
667
680
  */
668
- static build(data: TrackData, requester?: User | ClientUser): Track;
681
+ static build<T = User | ClientUser>(data: TrackData, requester?: T): Track;
669
682
  /**
670
683
  * Builds a UnresolvedTrack to be resolved before being played .
671
684
  * @param query
672
685
  * @param requester
673
686
  */
674
- static buildUnresolved(query: string | UnresolvedQuery, requester?: User | ClientUser): UnresolvedTrack;
687
+ static buildUnresolved<T = User | ClientUser>(query: string | UnresolvedQuery, requester?: T): UnresolvedTrack;
675
688
  static getClosestTrack(unresolvedTrack: UnresolvedTrack): Promise<Track>;
676
689
  }
677
690
  /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
@@ -862,6 +875,16 @@ declare class Manager extends EventEmitter {
862
875
  /** The options that were set. */
863
876
  readonly options: ManagerOptions;
864
877
  private initiated;
878
+ /** Loads player states from the JSON file. */
879
+ loadPlayerStates(nodeId: string): void;
880
+ /** Gets each player's JSON file */
881
+ private getPlayerFilePath;
882
+ /** Saves player states to the JSON file. */
883
+ savePlayerState(guildId: string): void;
884
+ /** Serializes a Player instance to avoid circular references. */
885
+ private serializePlayer;
886
+ /** Check for players that are no longer active */
887
+ private cleanupInactivePlayers;
865
888
  /** Returns the nodes that has the least load. */
866
889
  private get leastLoadNode();
867
890
  /** Returns the nodes that has the least amount of players. */
@@ -870,6 +893,18 @@ declare class Manager extends EventEmitter {
870
893
  private get priorityNode();
871
894
  /** Returns the node to use. */
872
895
  get useableNodes(): Node;
896
+ /** work in progress */
897
+ private lastProcessedGuilds;
898
+ private lastSaveTimes;
899
+ private saveInterval;
900
+ private saveQueues;
901
+ /** Register savePlayerStates events */
902
+ private registerPlayerStateEvents;
903
+ private handleEvent;
904
+ /** Queues a player state save */
905
+ private queuePlayerStateSave;
906
+ /** Processes the next queued save for a specific guild */
907
+ private processNextQueue;
873
908
  /**
874
909
  * Initiates the Manager class.
875
910
  * @param options
@@ -886,7 +921,7 @@ declare class Manager extends EventEmitter {
886
921
  * @param requester
887
922
  * @returns The search result.
888
923
  */
889
- search(query: string | SearchQuery, requester?: User | ClientUser): Promise<SearchResult>;
924
+ search<T = User | ClientUser>(query: string | SearchQuery, requester?: T): Promise<SearchResult>;
890
925
  private parseYouTubeTitle;
891
926
  private balanceBrackets;
892
927
  private escapeRegExp;
@@ -970,6 +1005,7 @@ interface ManagerOptions {
970
1005
  send(id: string, payload: Payload): void;
971
1006
  }
972
1007
  type SearchPlatform = "deezer" | "soundcloud" | "youtube music" | "youtube" | "spotify" | "jiosaavn" | "tidal" | "applemusic" | "bandcamp";
1008
+ type PlayerStateEventType = "connectionChange" | "playerCreate" | "playerDestroy" | "channelChange" | "volumeChange" | "pauseChange" | "queueChange" | "trackChange" | "repeatChange" | "autoplayChange";
973
1009
  interface SearchQuery {
974
1010
  /** The source to search from. */
975
1011
  source?: SearchPlatform | string;
@@ -1019,7 +1055,7 @@ interface ManagerEvents {
1019
1055
  nodeRaw: [payload: unknown];
1020
1056
  playerCreate: [player: Player];
1021
1057
  playerDestroy: [player: Player];
1022
- playerStateUpdate: [oldPlayer: Player, newPlayer: Player];
1058
+ playerStateUpdate: [oldPlayer: Player, newPlayer: Player, changeType: PlayerStateEventType];
1023
1059
  playerMove: [player: Player, initChannel: string, newChannel: string];
1024
1060
  playerDisconnect: [player: Player, oldChannel: string];
1025
1061
  queueEnd: [player: Player, track: Track | UnresolvedTrack, payload: TrackEndEvent];
@@ -1034,4 +1070,4 @@ interface ManagerEvents {
1034
1070
  chaptersLoaded: [player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockChaptersLoaded];
1035
1071
  }
1036
1072
 
1037
- export { type CPUStats, type EqualizerBand, type Exception, type Extendable, type FrameStats, type LavalinkInfo, type LavalinkResponse, type LoadType, Manager, type ManagerEvents, type ManagerOptions, type MemoryStats, Node, type NodeMessage, type NodeOptions, type NodeStats, type Payload, type PlayOptions, Player, type PlayerEvent, type PlayerEventType, type PlayerEvents, type PlayerOptions, type PlayerUpdate, type PlaylistData, type PlaylistRawData, Plugin, Queue, type SearchPlatform, type SearchQuery, type SearchResult, type Severity, type Sizes, type SponsorBlockChapterStarted, type SponsorBlockChaptersLoaded, type SponsorBlockSegment, type SponsorBlockSegmentEventType, type SponsorBlockSegmentEvents, type SponsorBlockSegmentSkipped, type SponsorBlockSegmentsLoaded, type State, Structure, type Track, type TrackData, type TrackDataInfo, type TrackEndEvent, type TrackEndReason, type TrackExceptionEvent, type TrackPluginInfo, type TrackSourceName, type TrackStartEvent, type TrackStuckEvent, TrackUtils, type UnresolvedQuery, type UnresolvedTrack, type VoicePacket, type VoiceServer, type VoiceState, type WebSocketClosedEvent, validSponsorBlocks };
1073
+ export { type CPUStats, type EqualizerBand, type Exception, type Extendable, type FrameStats, type LavalinkInfo, type LavalinkResponse, type LoadType, Manager, type ManagerEvents, type ManagerOptions, type MemoryStats, Node, type NodeMessage, type NodeOptions, type NodeStats, type Payload, type PlayOptions, Player, type PlayerEvent, type PlayerEventType, type PlayerEvents, type PlayerOptions, type PlayerStateEventType, type PlayerUpdate, type PlaylistData, type PlaylistRawData, Plugin, Queue, type SearchPlatform, type SearchQuery, type SearchResult, type Severity, type Sizes, type SponsorBlockChapterStarted, type SponsorBlockChaptersLoaded, type SponsorBlockSegment, type SponsorBlockSegmentEventType, type SponsorBlockSegmentEvents, type SponsorBlockSegmentSkipped, type SponsorBlockSegmentsLoaded, type State, Structure, type Track, type TrackData, type TrackDataInfo, type TrackEndEvent, type TrackEndReason, type TrackExceptionEvent, type TrackPluginInfo, type TrackSourceName, type TrackStartEvent, type TrackStuckEvent, TrackUtils, type UnresolvedQuery, type UnresolvedTrack, type VoicePacket, type VoiceServer, type VoiceState, type WebSocketClosedEvent, validSponsorBlocks };
@@ -8,6 +8,8 @@ const collection_1 = require("@discordjs/collection");
8
8
  const events_1 = require("events");
9
9
  const managerCheck_1 = tslib_1.__importDefault(require("../utils/managerCheck"));
10
10
  const blockedWords_1 = require("../config/blockedWords");
11
+ const fs_1 = tslib_1.__importDefault(require("fs"));
12
+ const path_1 = tslib_1.__importDefault(require("path"));
11
13
  /**
12
14
  * The main hub for interacting with Lavalink and using Magmastream,
13
15
  */
@@ -33,6 +35,136 @@ class Manager extends events_1.EventEmitter {
33
35
  /** The options that were set. */
34
36
  options;
35
37
  initiated = false;
38
+ /** Loads player states from the JSON file. */
39
+ loadPlayerStates(nodeId) {
40
+ const createTrackData = (song) => ({
41
+ encoded: song.track,
42
+ info: {
43
+ identifier: song.identifier,
44
+ isSeekable: song.isSeekable,
45
+ author: song.author,
46
+ length: song.duration,
47
+ isrc: song.isrc,
48
+ isStream: song.isStream,
49
+ title: song.title,
50
+ uri: song.uri,
51
+ artworkUrl: song.artworkUrl,
52
+ sourceName: song.sourceName,
53
+ },
54
+ pluginInfo: song.pluginInfo,
55
+ });
56
+ const playerStatesDir = path_1.default.join(process.cwd(), "node_modules", "magmastream", "dist", "sessionData", "players");
57
+ if (!fs_1.default.existsSync(playerStatesDir)) {
58
+ fs_1.default.mkdirSync(playerStatesDir, { recursive: true });
59
+ console.log(`Created directory at ${playerStatesDir}`);
60
+ }
61
+ const playerFiles = fs_1.default.readdirSync(playerStatesDir);
62
+ for (const file of playerFiles) {
63
+ const filePath = path_1.default.join(playerStatesDir, file);
64
+ const data = fs_1.default.readFileSync(filePath, "utf-8");
65
+ const state = JSON.parse(data);
66
+ if (state && typeof state === "object" && state.guild && state.node.options.identifier === nodeId) {
67
+ const playerOptions = {
68
+ guild: state.options.guild,
69
+ textChannel: state.options.textChannel,
70
+ voiceChannel: state.options.voiceChannel,
71
+ selfDeafen: state.options.selfDeafen,
72
+ volume: state.options.volume,
73
+ };
74
+ this.create(playerOptions);
75
+ }
76
+ const player = this.get(state.options.guild);
77
+ const tracks = [];
78
+ if (state.queue.current !== null) {
79
+ const currentTrack = state.queue.current;
80
+ tracks.push(Utils_1.TrackUtils.build(createTrackData(currentTrack), currentTrack.requester));
81
+ for (const key in state.queue) {
82
+ if (!isNaN(Number(key)) && key !== "current" && key !== "previous" && key !== "manager") {
83
+ const song = state.queue[key];
84
+ tracks.push(Utils_1.TrackUtils.build(createTrackData(song), song.requester));
85
+ }
86
+ }
87
+ player.queue.add(tracks);
88
+ }
89
+ if (state.paused)
90
+ player.pause(true);
91
+ player.setTrackRepeat(state.trackRepeat);
92
+ player.setQueueRepeat(state.queueRepeat);
93
+ if (state.dynamicRepeat) {
94
+ player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
95
+ }
96
+ if (state.isAutoplay) {
97
+ player.setAutoplay(state.isAutoplay, state.data.Internal_BotUser);
98
+ }
99
+ console.log(`Loaded player state for ${state.options.guild}.`);
100
+ }
101
+ console.log("Loaded player states from player files.");
102
+ }
103
+ /** Gets each player's JSON file */
104
+ getPlayerFilePath(guildId) {
105
+ const playerStateFilePath = path_1.default.join(process.cwd(), "node_modules", "magmastream", "dist", "sessionData", "players", `${guildId}.json`);
106
+ const configDir = path_1.default.dirname(playerStateFilePath);
107
+ if (!fs_1.default.existsSync(configDir)) {
108
+ fs_1.default.mkdirSync(configDir, { recursive: true });
109
+ console.log(`Created directory at: ${configDir}`);
110
+ }
111
+ return playerStateFilePath;
112
+ }
113
+ /** Saves player states to the JSON file. */
114
+ savePlayerState(guildId) {
115
+ const playerStateFilePath = this.getPlayerFilePath(guildId);
116
+ const player = this.players.get(guildId);
117
+ if (!player || player.state === "DISCONNECTED" || !player.voiceChannel)
118
+ return this.cleanupInactivePlayers();
119
+ const serializedPlayer = this.serializePlayer(player);
120
+ fs_1.default.writeFileSync(playerStateFilePath, JSON.stringify(serializedPlayer, null, 2), "utf-8");
121
+ console.log(`Saved ${guildId} player state to: ${playerStateFilePath}`);
122
+ }
123
+ /** Serializes a Player instance to avoid circular references. */
124
+ serializePlayer(player) {
125
+ const seen = new WeakSet();
126
+ const serialize = (obj) => {
127
+ if (obj && typeof obj === "object") {
128
+ if (seen.has(obj))
129
+ return; // Prevent circular references
130
+ seen.add(obj);
131
+ }
132
+ return obj;
133
+ };
134
+ const serializedPlayer = JSON.parse(JSON.stringify(player, (key, value) => {
135
+ if (key === "filters" || key === "manager") {
136
+ return null;
137
+ }
138
+ if (key === "queue") {
139
+ return {
140
+ ...value,
141
+ current: value.current || null,
142
+ };
143
+ }
144
+ // return value === undefined ? null : serialize(value);
145
+ return serialize(value);
146
+ }));
147
+ return serializedPlayer;
148
+ }
149
+ /** Check for players that are no longer active */
150
+ cleanupInactivePlayers() {
151
+ const playerStatesDir = path_1.default.join(process.cwd(), "node_modules", "magmastream", "dist", "sessionData", "players");
152
+ // Create the directory if it does not exist
153
+ if (!fs_1.default.existsSync(playerStatesDir)) {
154
+ fs_1.default.mkdirSync(playerStatesDir, { recursive: true });
155
+ console.log(`Created directory at ${playerStatesDir}`);
156
+ }
157
+ const playerFiles = fs_1.default.readdirSync(playerStatesDir);
158
+ const activeGuildIds = new Set(this.players.keys());
159
+ for (const file of playerFiles) {
160
+ const guildId = path_1.default.basename(file, ".json");
161
+ if (!activeGuildIds.has(guildId)) {
162
+ const filePath = path_1.default.join(playerStatesDir, file);
163
+ fs_1.default.unlinkSync(filePath);
164
+ console.log(`Deleted inactive player state file: ${filePath}`);
165
+ }
166
+ }
167
+ }
36
168
  /** Returns the nodes that has the least load. */
37
169
  get leastLoadNode() {
38
170
  return this.nodes
@@ -69,12 +201,74 @@ class Manager extends events_1.EventEmitter {
69
201
  get useableNodes() {
70
202
  return this.options.usePriority ? this.priorityNode : this.options.useNode === "leastLoad" ? this.leastLoadNode.first() : this.leastPlayersNode.first();
71
203
  }
204
+ /** work in progress */
205
+ lastProcessedGuilds = new Set();
206
+ lastSaveTimes = new Map();
207
+ saveInterval = 1000;
208
+ saveQueues = new Map();
209
+ /** Register savePlayerStates events */
210
+ registerPlayerStateEvents() {
211
+ const events = ["playerStateUpdate", "playerDestroy"];
212
+ for (const event of events) {
213
+ this.on(event, (player) => this.handleEvent(event, player));
214
+ }
215
+ }
216
+ handleEvent(event, player) {
217
+ switch (event) {
218
+ case "playerDestroy":
219
+ this.lastSaveTimes.delete(player.guild);
220
+ this.players.delete(player.guild);
221
+ this.cleanupInactivePlayers();
222
+ break;
223
+ case "playerStateUpdate":
224
+ this.queuePlayerStateSave(player);
225
+ break;
226
+ default:
227
+ this.savePlayerState(player.guild);
228
+ break;
229
+ }
230
+ }
231
+ /** Queues a player state save */
232
+ queuePlayerStateSave(player) {
233
+ const guildId = player.guild;
234
+ // If the current guild is not being processed, save immediately
235
+ if (!this.lastProcessedGuilds.has(guildId)) {
236
+ this.lastProcessedGuilds.add(guildId);
237
+ this.savePlayerState(guildId);
238
+ setTimeout(() => {
239
+ this.lastProcessedGuilds.delete(guildId);
240
+ this.processNextQueue(guildId);
241
+ }, this.saveInterval);
242
+ }
243
+ else {
244
+ if (!this.saveQueues.has(guildId)) {
245
+ this.saveQueues.set(guildId, []);
246
+ }
247
+ this.saveQueues.get(guildId).push(player);
248
+ }
249
+ }
250
+ /** Processes the next queued save for a specific guild */
251
+ processNextQueue(guildId) {
252
+ const queue = this.saveQueues.get(guildId);
253
+ if (queue && queue.length > 0) {
254
+ const player = queue.shift();
255
+ this.savePlayerState(player.guild);
256
+ if (queue.length === 0) {
257
+ this.saveQueues.delete(guildId);
258
+ }
259
+ setTimeout(() => this.processNextQueue(guildId), this.saveInterval);
260
+ }
261
+ else {
262
+ this.lastProcessedGuilds.delete(guildId);
263
+ }
264
+ }
72
265
  /**
73
266
  * Initiates the Manager class.
74
267
  * @param options
75
268
  */
76
269
  constructor(options) {
77
270
  super();
271
+ this.registerPlayerStateEvents();
78
272
  (0, managerCheck_1.default)(options);
79
273
  Utils_1.Structure.get("Player").init(this);
80
274
  Utils_1.Structure.get("Node").init(this);
@@ -316,6 +510,7 @@ class Manager extends events_1.EventEmitter {
316
510
  */
317
511
  destroy(guild) {
318
512
  this.players.delete(guild);
513
+ this.cleanupInactivePlayers();
319
514
  }
320
515
  /**
321
516
  * Creates a node or returns one if it already exists.
@@ -6,7 +6,16 @@ const Utils_1 = require("./Utils");
6
6
  const Rest_1 = require("./Rest");
7
7
  const nodeCheck_1 = tslib_1.__importDefault(require("../utils/nodeCheck"));
8
8
  const ws_1 = tslib_1.__importDefault(require("ws"));
9
+ const fs_1 = tslib_1.__importDefault(require("fs"));
10
+ const path_1 = tslib_1.__importDefault(require("path"));
9
11
  exports.validSponsorBlocks = ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler"];
12
+ const sessionIdsFilePath = path_1.default.join(process.cwd(), "node_modules", "magmastream", "dist", "sessionData", "sessionIds.json");
13
+ let sessionIdsMap = new Map();
14
+ const configDir = path_1.default.dirname(sessionIdsFilePath);
15
+ if (!fs_1.default.existsSync(configDir)) {
16
+ fs_1.default.mkdirSync(configDir, { recursive: true });
17
+ console.log(`Created directory at ${configDir}`);
18
+ }
10
19
  class Node {
11
20
  options;
12
21
  /** The socket for the node. */
@@ -88,6 +97,29 @@ class Node {
88
97
  this.manager.nodes.set(this.options.identifier, this);
89
98
  this.manager.emit("nodeCreate", this);
90
99
  this.rest = new Rest_1.Rest(this);
100
+ this.createSessionIdsFile(); // Create the session IDs file on initialization
101
+ this.loadSessionIds(); // Load session IDs on initialization
102
+ }
103
+ /** Creates the sessionIds.json file if it doesn't exist. */
104
+ createSessionIdsFile() {
105
+ if (!fs_1.default.existsSync(sessionIdsFilePath)) {
106
+ fs_1.default.writeFileSync(sessionIdsFilePath, JSON.stringify({}), "utf-8");
107
+ console.log(`Created sessionIds.json at ${sessionIdsFilePath}`);
108
+ }
109
+ }
110
+ /** Loads session IDs from the sessionIds.json file. */
111
+ loadSessionIds() {
112
+ if (fs_1.default.existsSync(sessionIdsFilePath)) {
113
+ const sessionIdsData = fs_1.default.readFileSync(sessionIdsFilePath, "utf-8");
114
+ sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
115
+ console.log(`Loaded session IDs from JSON file`);
116
+ }
117
+ }
118
+ /** Updates the session ID in the sessionIds.json file. */
119
+ updateSessionId() {
120
+ sessionIdsMap.set(this.options.identifier, this.sessionId); // Store session ID
121
+ fs_1.default.writeFileSync(sessionIdsFilePath, JSON.stringify(Object.fromEntries(sessionIdsMap))); // Update JSON file
122
+ console.log(`Updated session ID for ${this.options.identifier} to ${this.sessionId}`);
91
123
  }
92
124
  /** Connects to the Node. */
93
125
  connect() {
@@ -101,6 +133,11 @@ class Node {
101
133
  if (this.sessionId) {
102
134
  headers["Session-Id"] = this.sessionId;
103
135
  }
136
+ else if (this.options.resumeStatus && sessionIdsMap.has(this.options.identifier)) {
137
+ this.sessionId = sessionIdsMap.get(this.options.identifier) || null;
138
+ headers["Session-Id"] = this.sessionId;
139
+ console.log(`Resuming session with ID: ${this.sessionId}`);
140
+ }
104
141
  this.socket = new ws_1.default(`ws${this.options.secure ? "s" : ""}://${this.address}/v4/websocket`, { headers });
105
142
  this.socket.on("open", this.open.bind(this));
106
143
  this.socket.on("close", this.close.bind(this));
@@ -177,7 +214,14 @@ class Node {
177
214
  case "ready":
178
215
  this.rest.setSessionId(payload.sessionId);
179
216
  this.sessionId = payload.sessionId;
217
+ this.updateSessionId(); // Call to update session ID
180
218
  this.info = await this.fetchInfo();
219
+ // Log if the session was resumed successfully
220
+ if (payload.resumed) {
221
+ console.log(`Session resumed successfully for ${this.options.identifier}`);
222
+ // Load player states from the JSON file
223
+ await this.manager.loadPlayerStates(this.options.identifier);
224
+ }
181
225
  if (this.options.resumeStatus) {
182
226
  this.rest.patch(`/v4/sessions/${this.sessionId}`, {
183
227
  resuming: this.options.resumeStatus,
@@ -237,12 +281,15 @@ class Node {
237
281
  }
238
282
  }
239
283
  trackStart(player, track, payload) {
284
+ const oldPlayer = player;
240
285
  player.playing = true;
241
286
  player.paused = false;
242
287
  this.manager.emit("trackStart", player, track, payload);
288
+ this.manager.emit("playerStateUpdate", oldPlayer, player, "trackChange");
243
289
  }
244
290
  async trackEnd(player, track, payload) {
245
291
  const { reason } = payload;
292
+ const oldPlayer = player;
246
293
  // If the track failed to load or was cleaned up
247
294
  if (["loadFailed", "cleanup"].includes(reason)) {
248
295
  this.handleFailedTrack(player, track, payload);
@@ -264,6 +311,7 @@ class Node {
264
311
  else {
265
312
  await this.queueEnd(player, track, payload);
266
313
  }
314
+ this.manager.emit("playerStateUpdate", oldPlayer, player, "trackChange");
267
315
  }
268
316
  extractSpotifyTrackID(url) {
269
317
  const regex = /https:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]+)/;
@@ -3,13 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Player = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const Filters_1 = require("./Filters");
6
+ const Queue_1 = require("./Queue");
6
7
  const Utils_1 = require("./Utils");
7
8
  const _ = tslib_1.__importStar(require("lodash"));
8
9
  const playerCheck_1 = tslib_1.__importDefault(require("../utils/playerCheck"));
9
10
  class Player {
10
11
  options;
11
12
  /** The Queue for the Player. */
12
- queue = new (Utils_1.Structure.get("Queue"))();
13
+ queue;
13
14
  /** The filters applied to the audio. */
14
15
  filters;
15
16
  /** Whether the queue repeats the track. */
@@ -95,6 +96,8 @@ class Player {
95
96
  this.node = node || this.manager.useableNodes;
96
97
  if (!this.node)
97
98
  throw new RangeError("No available nodes.");
99
+ // Initialize the queue with guild and manager
100
+ this.queue = new Queue_1.Queue(this.guild, this.manager);
98
101
  this.manager.players.set(options.guild, this);
99
102
  this.manager.emit("playerCreate", this);
100
103
  this.setVolume(options.volume ?? 100);
@@ -124,7 +127,7 @@ class Player {
124
127
  },
125
128
  });
126
129
  this.state = "CONNECTED";
127
- this.manager.emit("playerStateUpdate", oldPlayer, this);
130
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "connectionChange");
128
131
  return this;
129
132
  }
130
133
  /** Disconnect from the voice channel. */
@@ -145,11 +148,12 @@ class Player {
145
148
  });
146
149
  this.voiceChannel = null;
147
150
  this.state = "DISCONNECTED";
148
- this.manager.emit("playerStateUpdate", oldPlayer, this);
151
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "connectionChange");
149
152
  return this;
150
153
  }
151
154
  /** Destroys the player. */
152
155
  destroy(disconnect = true) {
156
+ const oldPlayer = { ...this };
153
157
  this.state = "DESTROYING";
154
158
  if (disconnect) {
155
159
  this.disconnect();
@@ -157,6 +161,7 @@ class Player {
157
161
  this.node.rest.destroyPlayer(this.guild);
158
162
  this.manager.emit("playerDestroy", this);
159
163
  this.manager.players.delete(this.guild);
164
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "playerDestroy");
160
165
  }
161
166
  /**
162
167
  * Sets the player voice channel.
@@ -168,7 +173,7 @@ class Player {
168
173
  const oldPlayer = { ...this };
169
174
  this.voiceChannel = channel;
170
175
  this.connect();
171
- this.manager.emit("playerStateUpdate", oldPlayer, this);
176
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "channelChange");
172
177
  return this;
173
178
  }
174
179
  /**
@@ -180,7 +185,7 @@ class Player {
180
185
  throw new TypeError("Channel must be a non-empty string.");
181
186
  const oldPlayer = { ...this };
182
187
  this.textChannel = channel;
183
- this.manager.emit("playerStateUpdate", oldPlayer, this);
188
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "channelChange");
184
189
  return this;
185
190
  }
186
191
  /** Sets the now playing message. */
@@ -188,7 +193,8 @@ class Player {
188
193
  if (!message) {
189
194
  throw new TypeError("You must provide the message of the now playing message.");
190
195
  }
191
- return (this.nowPlayingMessage = message);
196
+ this.nowPlayingMessage = message;
197
+ return this.nowPlayingMessage;
192
198
  }
193
199
  async play(optionsOrTrack, playOptions) {
194
200
  if (typeof optionsOrTrack !== "undefined" && Utils_1.TrackUtils.validate(optionsOrTrack)) {
@@ -238,7 +244,7 @@ class Player {
238
244
  const oldPlayer = { ...this };
239
245
  this.isAutoplay = autoplayState;
240
246
  this.set("Internal_BotUser", botUser);
241
- this.manager.emit("playerStateUpdate", oldPlayer, this);
247
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "autoplayChange");
242
248
  return this;
243
249
  }
244
250
  /**
@@ -326,7 +332,7 @@ class Player {
326
332
  },
327
333
  });
328
334
  this.volume = volume;
329
- this.manager.emit("playerStateUpdate", oldPlayer, this);
335
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "volumeChange");
330
336
  return this;
331
337
  }
332
338
  /**
@@ -366,7 +372,7 @@ class Player {
366
372
  this.queueRepeat = false;
367
373
  this.dynamicRepeat = false;
368
374
  }
369
- this.manager.emit("playerStateUpdate", oldPlayer, this);
375
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
370
376
  return this;
371
377
  }
372
378
  /**
@@ -387,7 +393,7 @@ class Player {
387
393
  this.queueRepeat = false;
388
394
  this.dynamicRepeat = false;
389
395
  }
390
- this.manager.emit("playerStateUpdate", oldPlayer, this);
396
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
391
397
  return this;
392
398
  }
393
399
  /**
@@ -423,7 +429,7 @@ class Player {
423
429
  this.queueRepeat = false;
424
430
  this.dynamicRepeat = false;
425
431
  }
426
- this.manager.emit("playerStateUpdate", oldPlayer, this);
432
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
427
433
  return this;
428
434
  }
429
435
  /** Restarts the current track to the start. */
@@ -455,7 +461,7 @@ class Player {
455
461
  encodedTrack: null,
456
462
  },
457
463
  });
458
- this.manager.emit("playerStateUpdate", oldPlayer, this);
464
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
459
465
  return this;
460
466
  }
461
467
  /**
@@ -476,7 +482,7 @@ class Player {
476
482
  paused: pause,
477
483
  },
478
484
  });
479
- this.manager.emit("playerStateUpdate", oldPlayer, this);
485
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "pauseChange");
480
486
  return this;
481
487
  }
482
488
  /** Go back to the previous song. */
@@ -484,7 +490,7 @@ class Player {
484
490
  const oldPlayer = { ...this };
485
491
  this.queue.unshift(this.queue.previous);
486
492
  this.stop();
487
- this.manager.emit("playerStateUpdate", oldPlayer, this);
493
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
488
494
  return this;
489
495
  }
490
496
  /**
@@ -508,7 +514,7 @@ class Player {
508
514
  position: position,
509
515
  },
510
516
  });
511
- this.manager.emit("playerStateUpdate", oldPlayer, this);
517
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
512
518
  return this;
513
519
  }
514
520
  }
@@ -23,12 +23,22 @@ class Queue extends Array {
23
23
  current = null;
24
24
  /** The previous track */
25
25
  previous = null;
26
+ /** The Manager instance. */
27
+ manager;
28
+ /** The guild property. */
29
+ guild;
30
+ constructor(guild, manager) {
31
+ super();
32
+ this.manager = manager; // Initialize the manager
33
+ this.guild = guild; // Initialize the guild property
34
+ }
26
35
  /**
27
36
  * Adds a track to the queue.
28
37
  * @param track
29
38
  * @param [offset=null]
30
39
  */
31
40
  add(track, offset) {
41
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
32
42
  if (!Utils_1.TrackUtils.validate(track)) {
33
43
  throw new RangeError('Track must be a "Track" or "Track[]".');
34
44
  }
@@ -65,8 +75,10 @@ class Queue extends Array {
65
75
  }
66
76
  }
67
77
  }
78
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
68
79
  }
69
80
  remove(startOrPosition = 0, end) {
81
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
70
82
  if (typeof end !== "undefined") {
71
83
  if (isNaN(Number(startOrPosition)) || isNaN(Number(end))) {
72
84
  throw new RangeError(`Missing "start" or "end" parameter.`);
@@ -74,22 +86,31 @@ class Queue extends Array {
74
86
  if (startOrPosition >= end || startOrPosition >= this.length) {
75
87
  throw new RangeError("Invalid start or end values.");
76
88
  }
77
- return this.splice(startOrPosition, end - startOrPosition);
89
+ this.splice(startOrPosition, end - startOrPosition);
90
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
91
+ return;
78
92
  }
79
- return this.splice(startOrPosition, 1);
93
+ this.splice(startOrPosition, 1);
94
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
95
+ return;
80
96
  }
81
97
  /** Clears the queue. */
82
98
  clear() {
99
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
83
100
  this.splice(0);
101
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
84
102
  }
85
103
  /** Shuffles the queue. */
86
104
  shuffle() {
105
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
87
106
  for (let i = this.length - 1; i > 0; i--) {
88
107
  const j = Math.floor(Math.random() * (i + 1));
89
108
  [this[i], this[j]] = [this[j], this[i]];
90
109
  }
110
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
91
111
  }
92
112
  userBlockShuffle() {
113
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
93
114
  const userTracks = new Map();
94
115
  this.forEach((track) => {
95
116
  const user = track.requester.id;
@@ -107,10 +128,12 @@ class Queue extends Array {
107
128
  }
108
129
  });
109
130
  }
110
- this.clear();
131
+ this.splice(0);
111
132
  this.add(shuffledQueue);
133
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
112
134
  }
113
135
  roundRobinShuffle() {
136
+ const oldPlayer = { ...this.manager.players.get(this.guild) };
114
137
  const userTracks = new Map();
115
138
  this.forEach((track) => {
116
139
  const user = track.requester.id;
@@ -137,8 +160,9 @@ class Queue extends Array {
137
160
  }
138
161
  }
139
162
  }
140
- this.clear();
163
+ this.splice(0);
141
164
  this.add(shuffledQueue);
165
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
142
166
  }
143
167
  }
144
168
  exports.Queue = Queue;
@@ -39,6 +39,10 @@ class Rest {
39
39
  async destroyPlayer(guildId) {
40
40
  return await this.delete(`/v4/sessions/${this.sessionId}/players/${guildId}`);
41
41
  }
42
+ /** Updates the session status for resuming. */
43
+ async updateSession(resuming, timeout) {
44
+ return await this.patch(`/v4/sessions/${this.sessionId}`, { resuming, timeout });
45
+ }
42
46
  /* Sends a GET request to the specified endpoint and returns the response data. */
43
47
  async request(method, endpoint, body) {
44
48
  const config = {
@@ -17,9 +17,23 @@ class TrackUtils {
17
17
  static setTrackPartial(partial) {
18
18
  if (!Array.isArray(partial) || !partial.every((str) => typeof str === "string"))
19
19
  throw new Error("Provided partial is not an array or not a string array.");
20
- if (!partial.includes("track"))
21
- partial.unshift("track");
22
- this.trackPartial = partial;
20
+ const defaultProperties = [
21
+ "encoded",
22
+ "pluginInfo",
23
+ "identifier",
24
+ "isSeekable",
25
+ "author",
26
+ "length",
27
+ "isrc",
28
+ "isStream",
29
+ "title",
30
+ "uri",
31
+ "artworkUrl",
32
+ "sourceName",
33
+ ];
34
+ this.trackPartial = Array.from(new Set([...defaultProperties, ...partial]));
35
+ if (!this.trackPartial.includes("track"))
36
+ this.trackPartial.unshift("track");
23
37
  }
24
38
  /**
25
39
  * Checks if the provided argument is a valid Track or UnresolvedTrack, if provided an array then every element will be checked.
@@ -81,7 +95,7 @@ class TrackUtils {
81
95
  const finalSize = SIZES.find((s) => s === size) ?? "default";
82
96
  return this.uri.includes("youtube") ? `https://img.youtube.com/vi/${data.info.identifier}/${finalSize}.jpg` : null;
83
97
  },
84
- requester,
98
+ requester: requester,
85
99
  pluginInfo: data.pluginInfo,
86
100
  customData: {},
87
101
  };
@@ -112,7 +126,7 @@ class TrackUtils {
112
126
  if (typeof query === "undefined")
113
127
  throw new RangeError('Argument "query" must be present.');
114
128
  let unresolvedTrack = {
115
- requester,
129
+ requester: requester,
116
130
  async resolve() {
117
131
  const resolved = await TrackUtils.getClosestTrack(this);
118
132
  Object.getOwnPropertyNames(this).forEach((prop) => delete this[prop]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.6.0-beta.2",
3
+ "version": "2.6.0-beta.3",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",