magmastream 2.10.6-alpha.5 → 2.10.6-alpha.7

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.
@@ -38,6 +38,11 @@ export declare class Manager extends EventEmitter {
38
38
  */
39
39
  constructor(options: StandaloneManagerOptions);
40
40
  constructor(options: ManagerOptions, isWrapper: true);
41
+ /**
42
+ * Waits until the Redis client is connected, at most `timeoutMs`. Never throws: when Redis is not
43
+ * ready in time, init continues and commands fail later with Redis's own error.
44
+ */
45
+ private waitForRedis;
41
46
  /**
42
47
  * Initiates the Manager.
43
48
  * @param clientId - The Discord client ID (only required when not using any of the magmastream wrappers).
@@ -179,6 +179,30 @@ class Manager extends events_1.EventEmitter {
179
179
  });
180
180
  }
181
181
  }
182
+ /**
183
+ * Waits until the Redis client is connected, at most `timeoutMs`. Never throws: when Redis is not
184
+ * ready in time, init continues and commands fail later with Redis's own error.
185
+ */
186
+ async waitForRedis(timeoutMs = 10_000) {
187
+ const redis = this.redis;
188
+ if (!redis || redis.status === "ready")
189
+ return;
190
+ await new Promise((resolve) => {
191
+ const done = (message) => {
192
+ clearTimeout(timer);
193
+ redis.off("ready", onReady);
194
+ redis.off("end", onEnd);
195
+ if (message)
196
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] ${message}`);
197
+ resolve();
198
+ };
199
+ const onReady = () => done();
200
+ const onEnd = () => done("Redis connection ended before it was ready.");
201
+ const timer = setTimeout(() => done(`Redis was not ready after ${timeoutMs}ms, continuing without waiting.`), timeoutMs);
202
+ redis.once("ready", onReady);
203
+ redis.once("end", onEnd);
204
+ });
205
+ }
182
206
  /**
183
207
  * Initiates the Manager.
184
208
  * @param clientId - The Discord client ID (only required when not using any of the magmastream wrappers).
@@ -209,6 +233,9 @@ class Manager extends events_1.EventEmitter {
209
233
  }
210
234
  if (this.options.stateStorage.type === Enums_1.StateStorageType.Redis) {
211
235
  this.redis = new ioredis_1.default(lodash_1.default.omit(this.options.stateStorage.redisConfig, "prefix"));
236
+ // Nodes read and write their session ids as soon as they connect. With enableOfflineQueue: false,
237
+ // any command sent before Redis is ready fails, so wait for the connection first.
238
+ await this.waitForRedis();
212
239
  }
213
240
  const results = await Promise.allSettled([...this.nodes.values()].map(async (node) => {
214
241
  await node.connect();
@@ -12,6 +12,8 @@ const Enums_1 = require("./Enums");
12
12
  const MagmastreamError_1 = require("./MagmastreamError");
13
13
  const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((segment) => segment.toLowerCase());
14
14
  const TRACK_REQUIRED_EVENTS = new Set(["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"]);
15
+ /** How many recently played tracks autoplay avoids picking again. */
16
+ const AUTOPLAY_HISTORY_SIZE = 20;
15
17
  class Node {
16
18
  manager;
17
19
  options;
@@ -496,7 +498,14 @@ class Node {
496
498
  const hadPreviousSession = this.pendingResumeSessionId && this.pendingResumeSessionId !== payload.sessionId;
497
499
  this.pendingResumeSessionId = null;
498
500
  this.sessionId = payload.sessionId;
499
- await this.updateSessionId();
501
+ // Storing the session id only matters for resuming after a restart. A storage failure must not stop
502
+ // the node from loading its info and player states.
503
+ try {
504
+ await this.updateSessionId();
505
+ }
506
+ catch (err) {
507
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Could not store sessionId for node ${this.options.identifier}, session resume may not work after a restart: ${err instanceof Error ? (err.cause instanceof Error ? err.cause.message : err.message) : String(err)}`);
508
+ }
500
509
  this.info = await this.fetchInfo();
501
510
  this.isNodeLink = this.isNodeLink || Boolean(this.info.isNodelink);
502
511
  this.rest.isNodeLink = this.isNodeLink;
@@ -910,7 +919,15 @@ class Node {
910
919
  .toLowerCase()
911
920
  .replace(/\s+/g, "")
912
921
  .replace(/[^a-z0-9]/g, "");
913
- const filteredTracks = tracks.filter((track) => track.identifier !== lastTrack.identifier && track.uri !== lastTrack.uri && normalize(track.title) !== normalize(lastTrack.title));
922
+ const isSameTrack = (a, b) => a.identifier === b.identifier || a.uri === b.uri || normalize(a.title) === normalize(b.title);
923
+ // Sources answer a seed with the same tracks every time, so only checking the last track
924
+ // lets autoplay bounce between two songs. Skip recently played tracks, and only when every
925
+ // recommendation was played recently, fall back to skipping just the last track.
926
+ const recentTracks = PreviousQueue.slice(-AUTOPLAY_HISTORY_SIZE);
927
+ let filteredTracks = tracks.filter((track) => !recentTracks.some((recent) => isSameTrack(track, recent)));
928
+ if (!filteredTracks.length) {
929
+ filteredTracks = tracks.filter((track) => !isSameTrack(track, lastTrack));
930
+ }
914
931
  if (filteredTracks.length) {
915
932
  const randomTrack = filteredTracks[Math.floor(Math.random() * filteredTracks.length)];
916
933
  await player.queue.add(randomTrack);
@@ -238,7 +238,17 @@ class AutoPlayUtils {
238
238
  if (apiKey) {
239
239
  return await this.getRecommendedTracksFromLastFm(track, apiKey);
240
240
  }
241
- const enabledSources = node.info.sourceManagers;
241
+ // node.info is only set once the node's "ready" message is handled, so a just-connected node has none yet
242
+ let info = node.info;
243
+ if (!info) {
244
+ try {
245
+ info = node.info = await node.fetchInfo();
246
+ }
247
+ catch (error) {
248
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[AUTOPLAY] Node ${node.options.identifier} has no info yet: ${error instanceof Error ? error.message : String(error)}`);
249
+ }
250
+ }
251
+ const enabledSources = info?.sourceManagers ?? [];
242
252
  const autoPlaySearchPlatforms = this.manager.options.autoPlaySearchPlatforms;
243
253
  // Iterate over autoplay platforms in order of priority
244
254
  for (const platform of autoPlaySearchPlatforms) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.6-alpha.5",
3
+ "version": "2.10.6-alpha.7",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "prepare": "npm run build",
12
12
  "build": "tsc",
13
13
  "types": "rtb --dist dist",
14
- "test": "npm run build && node --test tests/player-state.test.cjs tests/hot-paths.test.cjs tests/restore-performance.test.cjs tests/lyrics.test.cjs",
14
+ "test": "npm run build && node --test tests/player-state.test.cjs tests/hot-paths.test.cjs tests/restore-performance.test.cjs tests/lyrics.test.cjs tests/autoplay.test.cjs tests/session-storage.test.cjs",
15
15
  "format": "prettier --write .",
16
16
  "format:check": "prettier --check .",
17
17
  "lint": "eslint \"src/**/*.{ts,js}\"",