magmastream 2.6.0-beta.8 → 2.6.1

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
@@ -44,6 +44,7 @@ Also you can join the [Discord Support Server](https://discord.com/invite/HV59Z3
44
44
  | [Lunio](https://discord.com/api/oauth2/authorize?client_id=945030475779551415&permissions=61991952&scope=bot+applications.commands) | vexi |
45
45
  | [JukeDisc](https://discord.com/oauth2/authorize?client_id=1109751797549105176&permissions=968552214080&scope=bot+applications.commands) | Theo |
46
46
  | [Cool Music](https://discord.com/oauth2/authorize?client_id=923529398425096193&permissions=12888394808&redirect_uri=https%3A%2F%2Fdiscord.gg%2Fcool-music-support-925619107460698202&response_type=code&scope=bot%20identify%20applications.commands) | Itz Random |
47
+ | [Soundy](https://dsc.gg/sndy) | iaMJ |
47
48
 
48
49
  If you want to add your own bot create a pull request with your bot added. Please add your full name.
49
50
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { Message, User, ClientUser } from 'discord.js';
3
2
  import WebSocket from 'ws';
4
3
  import { Collection } from '@discordjs/collection';
@@ -203,6 +202,8 @@ declare class Player {
203
202
  manager: Manager;
204
203
  /** The autoplay state of the player. */
205
204
  isAutoplay: boolean;
205
+ /** The number of times to try autoplay before emitting queueEnd. */
206
+ autoplayTries: number;
206
207
  private static _manager;
207
208
  private readonly data;
208
209
  private dynamicLoopInterval;
@@ -271,7 +272,7 @@ declare class Player {
271
272
  * @param autoplayState
272
273
  * @param botUser
273
274
  */
274
- setAutoplay(autoplayState: boolean, botUser: object): this;
275
+ setAutoplay(autoplayState: boolean, botUser: object, tries?: number): this;
275
276
  /**
276
277
  * Gets recommended tracks and returns an array of tracks.
277
278
  * @param track
@@ -527,7 +528,6 @@ declare class Node {
527
528
  extractSpotifyTrackID(url: string): string | null;
528
529
  extractSpotifyArtistID(url: string): string | null;
529
530
  private handleAutoplay;
530
- private handleSpotifyAutoplay;
531
531
  private handleFailedTrack;
532
532
  private handleRepeatedTrack;
533
533
  private playNextTrack;
@@ -912,9 +912,9 @@ declare class Manager extends EventEmitter {
912
912
  constructor(options: ManagerOptions);
913
913
  /**
914
914
  * Initiates the Manager.
915
- * @param clientId
915
+ * @param clientId - The Discord client ID (required).
916
916
  */
917
- init(clientId?: string): this;
917
+ init(clientId: string): this;
918
918
  /**
919
919
  * Searches the enabled sources based off the URL or the `source` property.
920
920
  * @param query
@@ -997,6 +997,11 @@ interface ManagerOptions {
997
997
  defaultSearchPlatform?: SearchPlatform;
998
998
  /** Whether the YouTube video titles should be replaced if the Author does not exactly match. */
999
999
  replaceYouTubeCredentials?: boolean;
1000
+ /** The last.fm API key.
1001
+ * If you need to create one go here: https://www.last.fm/api/account/create.
1002
+ * If you already have one, get it from here: https://www.last.fm/api/accounts.
1003
+ */
1004
+ lastFmApiKey: string;
1000
1005
  /**
1001
1006
  * Function to send data to the websocket.
1002
1007
  * @param id
@@ -1004,7 +1009,23 @@ interface ManagerOptions {
1004
1009
  */
1005
1010
  send(id: string, payload: Payload): void;
1006
1011
  }
1007
- type SearchPlatform = "deezer" | "soundcloud" | "youtube music" | "youtube" | "spotify" | "jiosaavn" | "tidal" | "applemusic" | "bandcamp";
1012
+ declare const UseNodeOptions: {
1013
+ readonly leastLoad: "leastLoad";
1014
+ readonly leastPlayers: "leastPlayers";
1015
+ };
1016
+ type UseNodeOption = keyof typeof UseNodeOptions;
1017
+ declare const SearchPlatforms: {
1018
+ readonly deezer: "deezer";
1019
+ readonly soundcloud: "soundcloud";
1020
+ readonly "youtube music": "youtube music";
1021
+ readonly youtube: "youtube";
1022
+ readonly spotify: "spotify";
1023
+ readonly jiosaavn: "jiosaavn";
1024
+ readonly tidal: "tidal";
1025
+ readonly applemusic: "applemusic";
1026
+ readonly bandcamp: "bandcamp";
1027
+ };
1028
+ type SearchPlatform = keyof typeof SearchPlatforms;
1008
1029
  type PlayerStateEventType = "connectionChange" | "playerCreate" | "playerDestroy" | "channelChange" | "volumeChange" | "pauseChange" | "queueChange" | "trackChange" | "repeatChange" | "autoplayChange";
1009
1030
  interface SearchQuery {
1010
1031
  /** The source to search from. */
@@ -1034,9 +1055,25 @@ interface PlaylistRawData {
1034
1055
  /** The tracks of the playlist */
1035
1056
  tracks: TrackData[];
1036
1057
  }
1058
+ interface PlaylistInfoData {
1059
+ /** Url to playlist. */
1060
+ url: string;
1061
+ /** Type is always playlist in that case. */
1062
+ type: string;
1063
+ /** ArtworkUrl of playlist */
1064
+ artworkUrl: string;
1065
+ /** Number of total tracks in playlist */
1066
+ totalTracks: number;
1067
+ /** Author of playlist */
1068
+ author: string;
1069
+ }
1037
1070
  interface PlaylistData {
1038
1071
  /** The playlist name. */
1039
1072
  name: string;
1073
+ /** Requester of playlist. */
1074
+ requester: User | ClientUser;
1075
+ /** More playlist information. */
1076
+ playlistInfo: PlaylistInfoData[];
1040
1077
  /** The length of the playlist. */
1041
1078
  duration: number;
1042
1079
  /** The songs of the playlist. */
@@ -1071,4 +1108,4 @@ interface ManagerEvents {
1071
1108
  chaptersLoaded: [player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockChaptersLoaded];
1072
1109
  }
1073
1110
 
1074
- 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 };
1111
+ 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 PlaylistInfoData, type PlaylistRawData, Plugin, Queue, type SearchPlatform, SearchPlatforms, 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 UseNodeOption, UseNodeOptions, type VoicePacket, type VoiceServer, type VoiceState, type WebSocketClosedEvent, validSponsorBlocks };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Manager = void 0;
3
+ exports.SearchPlatforms = exports.UseNodeOptions = exports.Manager = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  /* eslint-disable no-async-promise-executor */
6
6
  const Utils_1 = require("./Utils");
@@ -337,17 +337,19 @@ class Manager extends events_1.EventEmitter {
337
337
  }
338
338
  /**
339
339
  * Initiates the Manager.
340
- * @param clientId
340
+ * @param clientId - The Discord client ID (required).
341
341
  */
342
342
  init(clientId) {
343
- if (this.initiated)
343
+ if (this.initiated) {
344
344
  return this;
345
- if (typeof clientId !== "undefined")
346
- this.options.clientId = clientId;
347
- if (typeof this.options.clientId !== "string")
348
- throw new Error('"clientId" set is not type of "string"');
349
- if (!this.options.clientId)
350
- throw new Error('"clientId" is not set. Pass it in Manager#init() or as a option in the constructor.');
345
+ }
346
+ // Validate clientId
347
+ if (typeof clientId !== "string" || !/^\d+$/.test(clientId)) {
348
+ throw new Error('"clientId" must be a valid Discord client ID.');
349
+ }
350
+ // Set the validated clientId
351
+ this.options.clientId = clientId;
352
+ // Attempt to connect nodes
351
353
  for (const node of this.nodes.values()) {
352
354
  try {
353
355
  node.connect();
@@ -400,6 +402,8 @@ class Manager extends events_1.EventEmitter {
400
402
  if (res.loadType === "playlist") {
401
403
  playlist = {
402
404
  name: playlistData.info.name,
405
+ playlistInfo: playlistData.pluginInfo,
406
+ requester: requester,
403
407
  tracks: playlistData.tracks.map((track) => Utils_1.TrackUtils.build(track, requester)),
404
408
  duration: playlistData.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0),
405
409
  };
@@ -610,3 +614,18 @@ class Manager extends events_1.EventEmitter {
610
614
  }
611
615
  }
612
616
  exports.Manager = Manager;
617
+ exports.UseNodeOptions = {
618
+ leastLoad: "leastLoad",
619
+ leastPlayers: "leastPlayers",
620
+ };
621
+ exports.SearchPlatforms = {
622
+ deezer: "deezer",
623
+ soundcloud: "soundcloud",
624
+ "youtube music": "youtube music",
625
+ youtube: "youtube",
626
+ spotify: "spotify",
627
+ jiosaavn: "jiosaavn",
628
+ tidal: "tidal",
629
+ applemusic: "applemusic",
630
+ bandcamp: "bandcamp",
631
+ };
@@ -8,6 +8,7 @@ const nodeCheck_1 = tslib_1.__importDefault(require("../utils/nodeCheck"));
8
8
  const ws_1 = tslib_1.__importDefault(require("ws"));
9
9
  const fs_1 = tslib_1.__importDefault(require("fs"));
10
10
  const path_1 = tslib_1.__importDefault(require("path"));
11
+ const axios_1 = tslib_1.__importDefault(require("axios"));
11
12
  exports.validSponsorBlocks = ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler"];
12
13
  const sessionIdsFilePath = path_1.default.join(process.cwd(), "node_modules", "magmastream", "dist", "sessionData", "sessionIds.json");
13
14
  let sessionIdsMap = new Map();
@@ -367,72 +368,103 @@ class Node {
367
368
  return match ? match[1] : null;
368
369
  }
369
370
  // Handle autoplay
370
- async handleAutoplay(player, track) {
371
+ async handleAutoplay(player, track, attempt = 0) {
372
+ if (!player.isAutoplay || attempt === player.autoplayTries || !player.queue.previous)
373
+ return false;
371
374
  const previousTrack = player.queue.previous;
372
- if (!player.isAutoplay || !previousTrack)
373
- return;
374
- const hasSpotifyURL = ["spotify.com", "open.spotify.com"].some((url) => previousTrack.uri.includes(url));
375
- if (hasSpotifyURL) {
376
- const spotifySuccess = await this.handleSpotifyAutoplay(player); // Check if Spotify autoplay was successful
377
- if (spotifySuccess)
378
- return; // If successful, exit the function
375
+ const apiKey = this.manager.options.lastFmApiKey;
376
+ // If Last.fm API is not available and YouTube is not supported
377
+ if (!apiKey && !this.info.sourceManagers.includes("youtube"))
378
+ return false;
379
+ // Handle YouTube autoplay logic
380
+ if ((!apiKey && this.info.sourceManagers.includes("youtube")) ||
381
+ (attempt === player.autoplayTries - 1 && !(apiKey && player.autoplayTries === 1) && this.info.sourceManagers.includes("youtube"))) {
382
+ const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => previousTrack.uri.includes(url));
383
+ const videoID = hasYouTubeURL
384
+ ? previousTrack.uri.split("=").pop()
385
+ : (await player.search(`${previousTrack.author} - ${previousTrack.title}`, player.get("Internal_BotUser"))).tracks[0]?.uri.split("=").pop();
386
+ if (!videoID)
387
+ return false;
388
+ let randomIndex;
389
+ let searchURI;
390
+ do {
391
+ randomIndex = Math.floor(Math.random() * 23) + 2;
392
+ searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`;
393
+ } while (track.uri.includes(searchURI));
394
+ const res = await player.search(searchURI, player.get("Internal_BotUser"));
395
+ if (res.loadType === "empty" || res.loadType === "error")
396
+ return false;
397
+ const foundTrack = res.tracks.find((t) => t.uri !== track.uri && t.author !== track.author && t.title !== track.title);
398
+ if (!foundTrack)
399
+ return false;
400
+ player.queue.add(foundTrack);
401
+ player.play();
402
+ return true;
379
403
  }
380
- const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => previousTrack.uri.includes(url));
381
- let videoID = previousTrack.uri.substring(previousTrack.uri.indexOf("=") + 1);
382
- if (!hasYouTubeURL) {
383
- const res = await player.search(`${previousTrack.author} - ${previousTrack.title}`, player.get("Internal_BotUser"));
384
- videoID = res.tracks[0].uri.substring(res.tracks[0].uri.indexOf("=") + 1);
404
+ // Handle Last.fm-based autoplay logic
405
+ let { author: artist } = previousTrack;
406
+ const { title, uri } = previousTrack;
407
+ const enabledSources = this.info.sourceManagers;
408
+ const isSpotifyEnabled = enabledSources.includes("spotify");
409
+ const isSpotifyUri = uri.includes("spotify.com");
410
+ let selectedSource = null;
411
+ if (isSpotifyEnabled && isSpotifyUri) {
412
+ selectedSource = "spotify";
385
413
  }
386
- let randomIndex;
387
- let searchURI;
388
- do {
389
- randomIndex = Math.floor(Math.random() * 23) + 2;
390
- searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`;
391
- } while (track.uri.includes(searchURI));
392
- const res = await player.search(searchURI, player.get("Internal_BotUser"));
393
- if (res.loadType === "empty" || res.loadType === "error")
394
- return;
395
- let tracks = res.tracks;
396
- if (res.loadType === "playlist")
397
- tracks = res.playlist.tracks;
398
- const foundTrack = tracks.sort(() => Math.random() - 0.5).find((shuffledTrack) => shuffledTrack.uri !== track.uri);
399
- if (!foundTrack)
400
- return;
401
- if (this.manager.options.replaceYouTubeCredentials) {
402
- foundTrack.author = foundTrack.author.replace("- Topic", "");
403
- foundTrack.title = foundTrack.title.replace("Topic -", "");
404
- if (foundTrack.title.includes("-")) {
405
- const [author, title] = foundTrack.title.split("-").map((str) => str.trim());
406
- foundTrack.author = author;
407
- foundTrack.title = title;
414
+ else {
415
+ selectedSource = this.manager.options.defaultSearchPlatform;
416
+ }
417
+ if (!artist || !title) {
418
+ if (!title) {
419
+ const noTitleUrl = `https://ws.audioscrobbler.com/2.0/?method=artist.getTopTracks&artist=${artist}&autocorrect=1&api_key=${apiKey}&format=json`;
420
+ const response = await axios_1.default.get(noTitleUrl);
421
+ if (response.data.error || !response.data.toptracks?.track?.length)
422
+ return false;
423
+ const randomTrack = response.data.toptracks.track[Math.floor(Math.random() * response.data.toptracks.track.length)];
424
+ const res = await player.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: selectedSource }, player.get("Internal_BotUser"));
425
+ if (res.loadType === "empty" || res.loadType === "error")
426
+ return false;
427
+ const foundTrack = res.tracks.find((t) => t.uri !== track.uri);
428
+ if (!foundTrack)
429
+ return false;
430
+ player.queue.add(foundTrack);
431
+ player.play();
432
+ return true;
433
+ }
434
+ else if (!artist) {
435
+ const noArtistUrl = `https://ws.audioscrobbler.com/2.0/?method=track.search&track=${title}&api_key=${apiKey}&format=json`;
436
+ const response = await axios_1.default.get(noArtistUrl);
437
+ artist = response.data.results.trackmatches?.track?.[0]?.artist;
438
+ if (!artist)
439
+ return false;
408
440
  }
409
441
  }
410
- player.queue.add(foundTrack);
411
- player.play();
412
- }
413
- async handleSpotifyAutoplay(player) {
414
- const previousTrack = player.queue.previous;
415
- const node = this.manager.useableNodes;
416
- const isSpotifyPluginEnabled = node.info.plugins.some((plugin) => plugin.name === "lavasrc-plugin");
417
- const isSpotifySourceManagerEnabled = node.info.sourceManagers.includes("spotify");
418
- if (!isSpotifySourceManagerEnabled || !isSpotifyPluginEnabled)
419
- return false;
420
- const trackID = this.extractSpotifyTrackID(previousTrack.uri);
421
- const artistID = this.extractSpotifyArtistID(previousTrack.pluginInfo.artistUrl);
422
- let identifier = [trackID && `seed_tracks=${trackID}`, artistID && `seed_artists=${artistID}`].filter(Boolean).join("&");
423
- if (identifier) {
424
- identifier = `sprec:${identifier}`;
442
+ const url = `https://ws.audioscrobbler.com/2.0/?method=track.getSimilar&artist=${artist}&track=${title}&limit=10&autocorrect=1&api_key=${apiKey}&format=json`;
443
+ const response = await axios_1.default.get(url);
444
+ if (response.data.error || !response.data.similartracks?.track?.length) {
445
+ const retryUrl = `https://ws.audioscrobbler.com/2.0/?method=artist.getTopTracks&artist=${artist}&autocorrect=1&api_key=${apiKey}&format=json`;
446
+ const retryResponse = await axios_1.default.get(retryUrl);
447
+ if (retryResponse.data.error || !retryResponse.data.toptracks?.track?.length)
448
+ return false;
449
+ const randomTrack = retryResponse.data.toptracks.track[Math.floor(Math.random() * retryResponse.data.toptracks.track.length)];
450
+ const res = await player.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: selectedSource }, player.get("Internal_BotUser"));
451
+ if (res.loadType === "empty" || res.loadType === "error")
452
+ return false;
453
+ const foundTrack = res.tracks.find((t) => t.uri !== track.uri);
454
+ if (!foundTrack)
455
+ return false;
456
+ player.queue.add(foundTrack);
457
+ player.play();
458
+ return true;
425
459
  }
426
- if (!identifier)
427
- return false;
428
- const recommendedResult = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
429
- if (recommendedResult.loadType !== "playlist")
460
+ const randomTrack = response.data.similartracks.track[Math.floor(Math.random() * response.data.similartracks.track.length)];
461
+ const res = await player.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: selectedSource }, player.get("Internal_BotUser"));
462
+ if (res.loadType === "empty" || res.loadType === "error")
430
463
  return false;
431
- const playlistData = recommendedResult.data;
432
- const recommendedTrack = playlistData.tracks[0];
433
- if (!recommendedTrack)
464
+ const foundTrack = res.tracks.find((t) => t.uri !== track.uri);
465
+ if (!foundTrack)
434
466
  return false;
435
- player.queue.add(Utils_1.TrackUtils.build(recommendedTrack, player.get("Internal_BotUser")));
467
+ player.queue.add(foundTrack);
436
468
  player.play();
437
469
  return true;
438
470
  }
@@ -480,13 +512,22 @@ class Node {
480
512
  player.queue.previous = player.queue.current;
481
513
  player.queue.current = null;
482
514
  if (!player.isAutoplay) {
483
- player.queue.previous = player.queue.current;
484
- player.queue.current = null;
485
515
  player.playing = false;
486
516
  this.manager.emit("queueEnd", player, track, payload);
487
517
  return;
488
518
  }
489
- await this.handleAutoplay(player, track);
519
+ let attempts = 1;
520
+ let success = false;
521
+ while (attempts <= player.autoplayTries) {
522
+ success = await this.handleAutoplay(player, track, attempts);
523
+ if (success)
524
+ return;
525
+ attempts++;
526
+ }
527
+ // If all attempts fail, reset the player state and emit queueEnd
528
+ player.queue.previous = null;
529
+ player.playing = false;
530
+ this.manager.emit("queueEnd", player, track, payload);
490
531
  }
491
532
  trackStuck(player, track, payload) {
492
533
  player.stop();
@@ -47,6 +47,8 @@ class Player {
47
47
  manager;
48
48
  /** The autoplay state of the player. */
49
49
  isAutoplay = false;
50
+ /** The number of times to try autoplay before emitting queueEnd. */
51
+ autoplayTries = 3;
50
52
  static _manager;
51
53
  data = {};
52
54
  dynamicLoopInterval;
@@ -116,7 +118,7 @@ class Player {
116
118
  if (!this.voiceChannel)
117
119
  throw new RangeError("No voice channel has been set.");
118
120
  this.state = "CONNECTING";
119
- const oldPlayer = { ...this };
121
+ const oldPlayer = this ? { ...this } : null;
120
122
  this.manager.options.send(this.guild, {
121
123
  op: 4,
122
124
  d: {
@@ -135,7 +137,7 @@ class Player {
135
137
  if (this.voiceChannel === null)
136
138
  return this;
137
139
  this.state = "DISCONNECTING";
138
- const oldPlayer = { ...this };
140
+ const oldPlayer = this ? { ...this } : null;
139
141
  this.pause(true);
140
142
  this.manager.options.send(this.guild, {
141
143
  op: 4,
@@ -153,7 +155,7 @@ class Player {
153
155
  }
154
156
  /** Destroys the player. */
155
157
  destroy(disconnect = true) {
156
- const oldPlayer = { ...this };
158
+ const oldPlayer = this ? { ...this } : null;
157
159
  this.state = "DESTROYING";
158
160
  if (disconnect) {
159
161
  this.disconnect();
@@ -170,10 +172,10 @@ class Player {
170
172
  setVoiceChannel(channel) {
171
173
  if (typeof channel !== "string")
172
174
  throw new TypeError("Channel must be a non-empty string.");
173
- const oldPlayer = { ...this };
175
+ const oldPlayer = this ? { ...this } : null;
174
176
  this.voiceChannel = channel;
175
177
  this.connect();
176
- this.manager.emit("playerStateUpdate", oldPlayer, this, "channelChange");
178
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "voiceChannelChange");
177
179
  return this;
178
180
  }
179
181
  /**
@@ -183,9 +185,9 @@ class Player {
183
185
  setTextChannel(channel) {
184
186
  if (typeof channel !== "string")
185
187
  throw new TypeError("Channel must be a non-empty string.");
186
- const oldPlayer = { ...this };
188
+ const oldPlayer = this ? { ...this } : null;
187
189
  this.textChannel = channel;
188
- this.manager.emit("playerStateUpdate", oldPlayer, this, "channelChange");
190
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "textChannelChange");
189
191
  return this;
190
192
  }
191
193
  /** Sets the now playing message. */
@@ -234,15 +236,19 @@ class Player {
234
236
  * @param autoplayState
235
237
  * @param botUser
236
238
  */
237
- setAutoplay(autoplayState, botUser) {
239
+ setAutoplay(autoplayState, botUser, tries = 3) {
238
240
  if (typeof autoplayState !== "boolean") {
239
241
  throw new TypeError("autoplayState must be a boolean.");
240
242
  }
241
243
  if (typeof botUser !== "object") {
242
244
  throw new TypeError("botUser must be a user-object.");
243
245
  }
244
- const oldPlayer = { ...this };
246
+ if (typeof tries !== "number" || tries < 1) {
247
+ tries = 3; // Default to 3 if invalid
248
+ }
249
+ const oldPlayer = this ? { ...this } : null;
245
250
  this.isAutoplay = autoplayState;
251
+ this.autoplayTries = tries;
246
252
  this.set("Internal_BotUser", botUser);
247
253
  this.manager.emit("playerStateUpdate", oldPlayer, this, "autoplayChange");
248
254
  return this;
@@ -324,7 +330,7 @@ class Player {
324
330
  setVolume(volume) {
325
331
  if (isNaN(volume))
326
332
  throw new TypeError("Volume must be a number.");
327
- const oldPlayer = { ...this };
333
+ const oldPlayer = this ? { ...this } : null;
328
334
  this.node.rest.updatePlayer({
329
335
  guildId: this.options.guild,
330
336
  data: {
@@ -361,7 +367,7 @@ class Player {
361
367
  setTrackRepeat(repeat) {
362
368
  if (typeof repeat !== "boolean")
363
369
  throw new TypeError('Repeat can only be "true" or "false".');
364
- const oldPlayer = { ...this };
370
+ const oldPlayer = this ? { ...this } : null;
365
371
  if (repeat) {
366
372
  this.trackRepeat = true;
367
373
  this.queueRepeat = false;
@@ -372,7 +378,7 @@ class Player {
372
378
  this.queueRepeat = false;
373
379
  this.dynamicRepeat = false;
374
380
  }
375
- this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
381
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackRepeatChange");
376
382
  return this;
377
383
  }
378
384
  /**
@@ -382,7 +388,7 @@ class Player {
382
388
  setQueueRepeat(repeat) {
383
389
  if (typeof repeat !== "boolean")
384
390
  throw new TypeError('Repeat can only be "true" or "false".');
385
- const oldPlayer = { ...this };
391
+ const oldPlayer = this ? { ...this } : null;
386
392
  if (repeat) {
387
393
  this.trackRepeat = false;
388
394
  this.queueRepeat = true;
@@ -393,7 +399,7 @@ class Player {
393
399
  this.queueRepeat = false;
394
400
  this.dynamicRepeat = false;
395
401
  }
396
- this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
402
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "queueRepeatChange");
397
403
  return this;
398
404
  }
399
405
  /**
@@ -408,7 +414,7 @@ class Player {
408
414
  if (this.queue.size <= 1) {
409
415
  throw new RangeError("The queue size must be greater than 1.");
410
416
  }
411
- const oldPlayer = { ...this };
417
+ const oldPlayer = this ? { ...this } : null;
412
418
  if (repeat) {
413
419
  this.trackRepeat = false;
414
420
  this.queueRepeat = false;
@@ -429,7 +435,7 @@ class Player {
429
435
  this.queueRepeat = false;
430
436
  this.dynamicRepeat = false;
431
437
  }
432
- this.manager.emit("playerStateUpdate", oldPlayer, this, "repeatChange");
438
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "dynamicRepeatChange");
433
439
  return this;
434
440
  }
435
441
  /** Restarts the current track to the start. */
@@ -449,7 +455,7 @@ class Player {
449
455
  }
450
456
  /** Stops the current track, optionally give an amount to skip to, e.g 5 would play the 5th song. */
451
457
  stop(amount) {
452
- const oldPlayer = { ...this };
458
+ const oldPlayer = this ? { ...this } : null;
453
459
  if (typeof amount === "number" && amount > 1) {
454
460
  if (amount > this.queue.length)
455
461
  throw new RangeError("Cannot skip more than the queue length.");
@@ -461,7 +467,7 @@ class Player {
461
467
  encodedTrack: null,
462
468
  },
463
469
  });
464
- this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
470
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChangeStop");
465
471
  return this;
466
472
  }
467
473
  /**
@@ -473,7 +479,7 @@ class Player {
473
479
  throw new RangeError('Pause can only be "true" or "false".');
474
480
  if (this.paused === pause || !this.queue.totalSize)
475
481
  return this;
476
- const oldPlayer = { ...this };
482
+ const oldPlayer = this ? { ...this } : null;
477
483
  this.playing = !pause;
478
484
  this.paused = pause;
479
485
  this.node.rest.updatePlayer({
@@ -487,10 +493,10 @@ class Player {
487
493
  }
488
494
  /** Go back to the previous song. */
489
495
  previous() {
490
- const oldPlayer = { ...this };
496
+ const oldPlayer = this ? { ...this } : null;
491
497
  this.queue.unshift(this.queue.previous);
492
498
  this.stop();
493
- this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
499
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChangePrevious");
494
500
  return this;
495
501
  }
496
502
  /**
@@ -504,7 +510,7 @@ class Player {
504
510
  if (isNaN(position)) {
505
511
  throw new RangeError("Position must be a number.");
506
512
  }
507
- const oldPlayer = { ...this };
513
+ const oldPlayer = this ? { ...this } : null;
508
514
  if (position < 0 || position > this.queue.current.duration)
509
515
  position = Math.max(Math.min(position, this.queue.current.duration), 0);
510
516
  this.position = position;
@@ -514,7 +520,7 @@ class Player {
514
520
  position: position,
515
521
  },
516
522
  });
517
- this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChange");
523
+ this.manager.emit("playerStateUpdate", oldPlayer, this, "trackChangeSeek");
518
524
  return this;
519
525
  }
520
526
  }
@@ -40,7 +40,7 @@ class Queue extends Array {
40
40
  add(track, offset) {
41
41
  const trackInfo = Array.isArray(track) ? track.map((t) => JSON.stringify(t, null, 2)).join(", ") : JSON.stringify(track, null, 2);
42
42
  this.manager.emit("debug", `[QUEUE] Added ${Array.isArray(track) ? track.length : 1} track(s) to queue: ${trackInfo}`);
43
- const oldPlayer = { ...this.manager.players.get(this.guild) };
43
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
44
44
  if (!Utils_1.TrackUtils.validate(track)) {
45
45
  throw new RangeError('Track must be a "Track" or "Track[]".');
46
46
  }
@@ -77,10 +77,10 @@ class Queue extends Array {
77
77
  }
78
78
  }
79
79
  }
80
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
80
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueTrackAdd");
81
81
  }
82
82
  remove(startOrPosition = 0, end) {
83
- const oldPlayer = { ...this.manager.players.get(this.guild) };
83
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
84
84
  if (typeof end !== "undefined") {
85
85
  // Validate input for `start` and `end`
86
86
  if (isNaN(Number(startOrPosition)) || isNaN(Number(end))) {
@@ -91,34 +91,34 @@ class Queue extends Array {
91
91
  }
92
92
  const removedTracks = this.splice(startOrPosition, end - startOrPosition);
93
93
  this.manager.emit("debug", `[QUEUE] Removed ${removedTracks.length} track(s) from player: ${this.guild} from position ${startOrPosition} to ${end}.`);
94
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
94
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueTrackRemove");
95
95
  return;
96
96
  }
97
97
  // Single item removal when no end specified
98
98
  const removedTrack = this.splice(startOrPosition, 1);
99
99
  this.manager.emit("debug", `[QUEUE] Removed 1 track from player: ${this.guild} from position ${startOrPosition}: ${JSON.stringify(removedTrack[0], null, 2)}`);
100
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
100
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueTrackRemoveSingle");
101
101
  return;
102
102
  }
103
103
  /** Clears the queue. */
104
104
  clear() {
105
- const oldPlayer = { ...this.manager.players.get(this.guild) };
105
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
106
106
  this.splice(0);
107
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
107
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueClear");
108
108
  this.manager.emit("debug", `[QUEUE] Cleared the queue for: ${this.guild}`);
109
109
  }
110
110
  /** Shuffles the queue. */
111
111
  shuffle() {
112
- const oldPlayer = { ...this.manager.players.get(this.guild) };
112
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
113
113
  for (let i = this.length - 1; i > 0; i--) {
114
114
  const j = Math.floor(Math.random() * (i + 1));
115
115
  [this[i], this[j]] = [this[j], this[i]];
116
116
  }
117
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
117
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueShuffle");
118
118
  this.manager.emit("debug", `[QUEUE] Shuffled the queue for: ${this.guild}`);
119
119
  }
120
120
  userBlockShuffle() {
121
- const oldPlayer = { ...this.manager.players.get(this.guild) };
121
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
122
122
  const userTracks = new Map();
123
123
  this.forEach((track) => {
124
124
  const user = track.requester.id;
@@ -138,11 +138,11 @@ class Queue extends Array {
138
138
  }
139
139
  this.splice(0);
140
140
  this.add(shuffledQueue);
141
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
141
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueUserBlockShuffle");
142
142
  this.manager.emit("debug", `[QUEUE] userBlockShuffled the queue for: ${this.guild}`);
143
143
  }
144
144
  roundRobinShuffle() {
145
- const oldPlayer = { ...this.manager.players.get(this.guild) };
145
+ const oldPlayer = this.manager.players.get(this.guild) ? { ...this.manager.players.get(this.guild) } : null;
146
146
  const userTracks = new Map();
147
147
  this.forEach((track) => {
148
148
  const user = track.requester.id;
@@ -171,7 +171,7 @@ class Queue extends Array {
171
171
  }
172
172
  this.splice(0);
173
173
  this.add(shuffledQueue);
174
- this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueChange");
174
+ this.manager.emit("playerStateUpdate", oldPlayer, this.manager.players.get(this.guild), "queueRobinShuffle");
175
175
  this.manager.emit("debug", `[QUEUE] roundRobinShuffled the queue for: ${this.guild}`);
176
176
  }
177
177
  }
@@ -1,22 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = managerCheck;
4
+ const Manager_1 = require("../structures/Manager");
3
5
  function managerCheck(options) {
4
6
  if (!options)
5
7
  throw new TypeError("ManagerOptions must not be empty.");
6
- const { autoPlay, clientId, clientName, defaultSearchPlatform, nodes, plugins, send, trackPartial, usePriority, useNode, replaceYouTubeCredentials } = options;
7
- if (typeof autoPlay !== "undefined" && typeof autoPlay !== "boolean") {
8
+ const { autoPlay, clientName, defaultSearchPlatform, nodes, plugins, send, trackPartial, usePriority, useNode, replaceYouTubeCredentials, lastFmApiKey } = options;
9
+ if (typeof autoPlay !== "boolean") {
8
10
  throw new TypeError('Manager option "autoPlay" must be a boolean.');
9
11
  }
10
- if (typeof clientId !== "undefined" && !/^\d+$/.test(clientId)) {
11
- throw new TypeError('Manager option "clientId" must be a non-empty string.');
12
- }
13
- if (typeof clientName !== "undefined" && typeof clientName !== "string") {
14
- throw new TypeError('Manager option "clientName" must be a string.');
12
+ if (typeof clientName !== "undefined") {
13
+ if (typeof clientName !== "string" || clientName.trim().length === 0) {
14
+ throw new TypeError('Manager option "clientName" must be a non-empty string.');
15
+ }
15
16
  }
16
- if (typeof defaultSearchPlatform !== "undefined" && typeof defaultSearchPlatform !== "string") {
17
- throw new TypeError('Manager option "defaultSearchPlatform" must be a string.');
17
+ if (typeof defaultSearchPlatform !== "undefined") {
18
+ if (typeof defaultSearchPlatform !== "string" || !Object.values(Manager_1.SearchPlatforms).includes(defaultSearchPlatform)) {
19
+ throw new TypeError(`Manager option "defaultSearchPlatform" must be one of: ${Object.values(Manager_1.SearchPlatforms).join(", ")}.`);
20
+ }
18
21
  }
19
- if (typeof nodes !== "undefined" && !Array.isArray(nodes)) {
22
+ if (typeof nodes === "undefined" || !Array.isArray(nodes)) {
20
23
  throw new TypeError('Manager option "nodes" must be an array.');
21
24
  }
22
25
  if (typeof plugins !== "undefined" && !Array.isArray(plugins)) {
@@ -25,16 +28,21 @@ function managerCheck(options) {
25
28
  if (typeof send !== "function") {
26
29
  throw new TypeError('Manager option "send" must be present and a function.');
27
30
  }
28
- if (typeof trackPartial !== "undefined" && !Array.isArray(trackPartial)) {
29
- throw new TypeError('Manager option "trackPartial" must be a string array.');
31
+ if (typeof trackPartial !== "undefined") {
32
+ if (!Array.isArray(trackPartial)) {
33
+ throw new TypeError('Manager option "trackPartial" must be an array.');
34
+ }
35
+ if (!trackPartial.every(item => typeof item === "string")) {
36
+ throw new TypeError('Manager option "trackPartial" must be an array of strings.');
37
+ }
30
38
  }
31
39
  if (typeof usePriority !== "undefined" && typeof usePriority !== "boolean") {
32
40
  throw new TypeError('Manager option "usePriority" must be a boolean.');
33
41
  }
34
42
  if (usePriority) {
35
43
  for (let index = 0; index < nodes.length; index++) {
36
- if (!nodes[index].priority) {
37
- throw new TypeError(`Missing node option "priority" at position ${index}`);
44
+ if (typeof nodes[index].priority !== 'number' || isNaN(nodes[index].priority)) {
45
+ throw new TypeError(`Missing or invalid node option "priority" at position ${index}`);
38
46
  }
39
47
  }
40
48
  }
@@ -42,12 +50,14 @@ function managerCheck(options) {
42
50
  if (typeof useNode !== "string") {
43
51
  throw new TypeError('Manager option "useNode" must be a string "leastLoad" or "leastPlayers".');
44
52
  }
45
- if (useNode !== "leastLoad" && useNode !== "leastPlayers") {
46
- throw new TypeError('Manager option must be either "leastLoad" or "leastPlayers".');
53
+ if (!(useNode in Manager_1.UseNodeOptions)) {
54
+ throw new TypeError('Manager option "useNode" must be either "leastLoad" or "leastPlayers".');
47
55
  }
48
56
  }
49
57
  if (typeof replaceYouTubeCredentials !== "undefined" && typeof replaceYouTubeCredentials !== "boolean") {
50
58
  throw new TypeError('Manager option "replaceYouTubeCredentials" must be a boolean.');
51
59
  }
60
+ if (typeof lastFmApiKey !== "undefined" && (typeof lastFmApiKey !== "string" || lastFmApiKey.trim().length === 0)) {
61
+ throw new TypeError('Manager option "lastFmApiKey" must be a non-empty string.');
62
+ }
52
63
  }
53
- exports.default = managerCheck;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = nodeCheck;
3
4
  function nodeCheck(options) {
4
5
  if (!options)
5
6
  throw new TypeError("NodeOptions must not be empty.");
@@ -38,4 +39,3 @@ function nodeCheck(options) {
38
39
  throw new TypeError('Node option "priority" must be a number.');
39
40
  }
40
41
  }
41
- exports.default = nodeCheck;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = playerCheck;
3
4
  function playerCheck(options) {
4
5
  if (!options)
5
6
  throw new TypeError("PlayerOptions must not be empty.");
@@ -26,4 +27,3 @@ function playerCheck(options) {
26
27
  throw new TypeError('Player option "volume" must be a number.');
27
28
  }
28
29
  }
29
- exports.default = playerCheck;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.6.0-beta.8",
3
+ "version": "2.6.1",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,23 +15,23 @@
15
15
  },
16
16
  "devDependencies": {
17
17
  "@favware/rollup-type-bundler": "^3.3.0",
18
- "@types/lodash": "^4.17.9",
19
- "@types/node": "^20.16.10",
20
- "@types/ws": "^8.5.12",
18
+ "@types/lodash": "^4.17.14",
19
+ "@types/node": "^20.17.12",
20
+ "@types/ws": "^8.5.13",
21
21
  "@typescript-eslint/eslint-plugin": "^7.18.0",
22
22
  "@typescript-eslint/parser": "^7.18.0",
23
23
  "eslint": "^8.57.1",
24
24
  "npm-run-all": "^4.1.5",
25
- "typedoc": "^0.25.13",
26
- "typedoc-plugin-no-inherit": "^1.4.0",
27
- "typescript": "^5.4.5"
25
+ "typedoc": "^0.27.6",
26
+ "typedoc-plugin-no-inherit": "^1.5.0",
27
+ "typescript": "^5.7.3"
28
28
  },
29
29
  "dependencies": {
30
30
  "@discordjs/collection": "^2.1.1",
31
- "axios": "^1.7.7",
31
+ "axios": "^1.7.9",
32
32
  "events": "^3.3.0",
33
33
  "lodash": "^4.17.21",
34
- "tslib": "^2.7.0",
34
+ "tslib": "^2.8.1",
35
35
  "ws": "^8.18.0"
36
36
  },
37
37
  "peerDependencies": {