magmastream 2.9.0-dev.4 → 2.9.0-dev.41

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.
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TrackSourceTypes = exports.SeverityTypes = exports.TrackEndReasonTypes = exports.StateTypes = exports.LoadTypes = exports.Structure = exports.AutoPlayUtils = exports.TrackUtils = void 0;
3
+ exports.Structure = exports.AutoPlayUtils = exports.TrackUtils = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const Manager_1 = require("./Manager");
5
+ /* eslint-disable @typescript-eslint/no-require-imports */
6
6
  const axios_1 = tslib_1.__importDefault(require("axios"));
7
7
  const jsdom_1 = require("jsdom");
8
- const crypto_1 = tslib_1.__importDefault(require("crypto"));
8
+ const Enums_1 = require("./Enums");
9
+ // import playwright from "playwright";
9
10
  /** @hidden */
10
11
  const SIZES = ["0", "1", "2", "3", "default", "mqdefault", "hqdefault", "maxresdefault"];
11
12
  class TrackUtils {
@@ -29,27 +30,27 @@ class TrackUtils {
29
30
  if (!Array.isArray(partial) || !partial.every((str) => typeof str === "string"))
30
31
  throw new Error("Provided partial is not an array or not a string array.");
31
32
  const defaultProperties = [
32
- Manager_1.TrackPartial.Track,
33
- Manager_1.TrackPartial.Title,
34
- Manager_1.TrackPartial.Identifier,
35
- Manager_1.TrackPartial.Author,
36
- Manager_1.TrackPartial.Duration,
37
- Manager_1.TrackPartial.Isrc,
38
- Manager_1.TrackPartial.IsSeekable,
39
- Manager_1.TrackPartial.IsStream,
40
- Manager_1.TrackPartial.Uri,
41
- Manager_1.TrackPartial.ArtworkUrl,
42
- Manager_1.TrackPartial.SourceName,
43
- Manager_1.TrackPartial.ThumbNail,
44
- Manager_1.TrackPartial.Requester,
45
- Manager_1.TrackPartial.PluginInfo,
46
- Manager_1.TrackPartial.CustomData,
33
+ Enums_1.TrackPartial.Track,
34
+ Enums_1.TrackPartial.Title,
35
+ Enums_1.TrackPartial.Identifier,
36
+ Enums_1.TrackPartial.Author,
37
+ Enums_1.TrackPartial.Duration,
38
+ Enums_1.TrackPartial.Isrc,
39
+ Enums_1.TrackPartial.IsSeekable,
40
+ Enums_1.TrackPartial.IsStream,
41
+ Enums_1.TrackPartial.Uri,
42
+ Enums_1.TrackPartial.ArtworkUrl,
43
+ Enums_1.TrackPartial.SourceName,
44
+ Enums_1.TrackPartial.ThumbNail,
45
+ Enums_1.TrackPartial.Requester,
46
+ Enums_1.TrackPartial.PluginInfo,
47
+ Enums_1.TrackPartial.CustomData,
47
48
  ];
48
49
  /** The array of property names that will be removed from the Track class */
49
50
  this.trackPartial = Array.from(new Set([...defaultProperties, ...partial]));
50
51
  /** Make sure that the "track" property is always included */
51
- if (!this.trackPartial.includes(Manager_1.TrackPartial.Track))
52
- this.trackPartial.unshift(Manager_1.TrackPartial.Track);
52
+ if (!this.trackPartial.includes(Enums_1.TrackPartial.Track))
53
+ this.trackPartial.unshift(Enums_1.TrackPartial.Track);
53
54
  }
54
55
  /**
55
56
  * Checks if the provided argument is a valid Track.
@@ -129,19 +130,32 @@ class TrackUtils {
129
130
  throw new RangeError(`Argument "data" is not a valid track: ${error.message}`);
130
131
  }
131
132
  }
133
+ /**
134
+ * Validates a search result.
135
+ * @param result The search result to validate.
136
+ * @returns Whether the search result is valid.
137
+ */
138
+ static isErrorOrEmptySearchResult(result) {
139
+ return result.loadType === Enums_1.LoadTypes.Empty || result.loadType === Enums_1.LoadTypes.Error;
140
+ }
132
141
  }
133
142
  exports.TrackUtils = TrackUtils;
134
143
  class AutoPlayUtils {
135
144
  static manager;
145
+ // private static cachedAccessToken: string | null = null;
146
+ // private static cachedAccessTokenExpiresAt: number = 0;
136
147
  /**
137
148
  * Initializes the AutoPlayUtils class with the given manager.
138
149
  * @param manager The manager instance to use.
139
150
  * @hidden
140
151
  */
141
- static init(manager) {
152
+ static async init(manager) {
142
153
  if (!manager)
143
154
  throw new Error("AutoPlayUtils.init() requires a valid Manager instance.");
144
155
  this.manager = manager;
156
+ // if (this.manager.options.autoPlaySearchPlatforms.includes(AutoPlayPlatform.Spotify)) {
157
+ // await this.getSpotifyAccessToken();
158
+ // }
145
159
  }
146
160
  /**
147
161
  * Gets recommended tracks for the given track.
@@ -154,6 +168,10 @@ class AutoPlayUtils {
154
168
  throw new Error("No available nodes.");
155
169
  }
156
170
  const apiKey = this.manager.options.lastFmApiKey;
171
+ // Check if Last.fm API is available
172
+ if (apiKey) {
173
+ return await this.getRecommendedTracksFromLastFm(track, apiKey);
174
+ }
157
175
  const enabledSources = node.info.sourceManagers;
158
176
  const autoPlaySearchPlatforms = this.manager.options.autoPlaySearchPlatforms;
159
177
  // Iterate over autoplay platforms in order of priority
@@ -166,10 +184,6 @@ class AutoPlayUtils {
166
184
  }
167
185
  }
168
186
  }
169
- // Check if Last.fm API is available
170
- if (apiKey) {
171
- return await this.getRecommendedTracksFromLastFm(track, apiKey);
172
- }
173
187
  return [];
174
188
  }
175
189
  /**
@@ -190,15 +204,10 @@ class AutoPlayUtils {
190
204
  return [];
191
205
  }
192
206
  const randomTrack = response.data.toptracks.track[Math.floor(Math.random() * response.data.toptracks.track.length)];
193
- const res = await this.manager.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: this.manager.options.defaultSearchPlatform }, track.requester);
194
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
207
+ const resolvedTracks = await this.resolveTracksFromQuery(`${randomTrack.artist.name} - ${randomTrack.name}`, this.manager.options.defaultSearchPlatform, track.requester);
208
+ if (!resolvedTracks.length)
195
209
  return [];
196
- }
197
- const filteredTracks = res.tracks.filter((t) => t.uri !== track.uri);
198
- if (!filteredTracks.length) {
199
- return [];
200
- }
201
- return filteredTracks;
210
+ return resolvedTracks;
202
211
  }
203
212
  if (!artist) {
204
213
  // No artist provided, search for the track title
@@ -228,11 +237,10 @@ class AutoPlayUtils {
228
237
  return [];
229
238
  }
230
239
  const randomTrack = retryResponse.data.toptracks.track[Math.floor(Math.random() * retryResponse.data.toptracks.track.length)];
231
- const res = await this.manager.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: this.manager.options.defaultSearchPlatform }, track.requester);
232
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
240
+ const resolvedTracks = await this.resolveTracksFromQuery(`${randomTrack.artist.name} - ${randomTrack.name}`, this.manager.options.defaultSearchPlatform, track.requester);
241
+ if (!resolvedTracks.length)
233
242
  return [];
234
- }
235
- const filteredTracks = res.tracks.filter((t) => t.uri !== track.uri);
243
+ const filteredTracks = resolvedTracks.filter((t) => t.uri !== track.uri);
236
244
  if (!filteredTracks.length) {
237
245
  return [];
238
246
  }
@@ -242,16 +250,10 @@ class AutoPlayUtils {
242
250
  if (!randomTrack) {
243
251
  return [];
244
252
  }
245
- const res = await this.manager.search({ query: `${randomTrack.artist.name} - ${randomTrack.name}`, source: this.manager.options.defaultSearchPlatform }, track.requester);
246
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
247
- return [];
248
- }
249
- if (res.loadType === LoadTypes.Playlist)
250
- res.tracks = res.playlist.tracks;
251
- if (!res.tracks.length) {
253
+ const resolvedTracks = await this.resolveTracksFromQuery(`${randomTrack.artist.name} - ${randomTrack.name}`, this.manager.options.defaultSearchPlatform, track.requester);
254
+ if (!resolvedTracks.length)
252
255
  return [];
253
- }
254
- return res.tracks;
256
+ return resolvedTracks;
255
257
  }
256
258
  /**
257
259
  * Gets recommended tracks from the given source.
@@ -260,157 +262,47 @@ class AutoPlayUtils {
260
262
  * @returns An array of recommended tracks.
261
263
  */
262
264
  static async getRecommendedTracksFromSource(track, platform) {
265
+ const requester = track.requester;
266
+ const parsedURL = new URL(track.uri);
263
267
  switch (platform) {
264
- case "spotify":
265
- try {
266
- if (!track.uri.includes("spotify")) {
267
- const res = await this.manager.search({ query: `${track.author} - ${track.title}`, source: Manager_1.SearchPlatform.Spotify }, track.requester);
268
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
269
- return [];
270
- }
271
- if (res.loadType === LoadTypes.Playlist) {
272
- res.tracks = res.playlist.tracks;
273
- }
274
- if (!res.tracks.length) {
275
- return [];
276
- }
277
- track = res.tracks[0];
278
- }
279
- const TOTP_SECRET = new Uint8Array([
280
- 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,
281
- ]);
282
- const hmac = crypto_1.default.createHmac("sha1", TOTP_SECRET);
283
- function generateTotp() {
284
- const counter = Math.floor(Date.now() / 30000);
285
- const counterBuffer = Buffer.alloc(8);
286
- counterBuffer.writeBigInt64BE(BigInt(counter));
287
- hmac.update(counterBuffer);
288
- const hmacResult = hmac.digest();
289
- const offset = hmacResult[hmacResult.length - 1] & 15;
290
- const truncatedValue = ((hmacResult[offset] & 127) << 24) | ((hmacResult[offset + 1] & 255) << 16) | ((hmacResult[offset + 2] & 255) << 8) | (hmacResult[offset + 3] & 255);
291
- const totp = (truncatedValue % 1000000).toString().padStart(6, "0");
292
- return [totp, counter * 30000];
293
- }
294
- const [totp, timestamp] = generateTotp();
295
- const params = {
296
- reason: "transport",
297
- productType: "embed",
298
- totp: totp,
299
- totpVer: 5,
300
- ts: timestamp,
301
- };
302
- let body;
303
- try {
304
- const response = await axios_1.default.get("https://open.spotify.com/get_access_token", { params });
305
- body = response.data;
306
- }
307
- catch (error) {
308
- console.error("[AutoPlay] Failed to get spotify access token:", error.response?.status, error.response?.data || error.message);
309
- return [];
310
- }
311
- let json;
312
- try {
313
- const response = await axios_1.default.get(`https://api.spotify.com/v1/recommendations`, {
314
- params: { limit: 10, seed_tracks: track.identifier },
315
- headers: {
316
- Authorization: `Bearer ${body.accessToken}`,
317
- "Content-Type": "application/json",
318
- },
319
- });
320
- json = response.data;
321
- }
322
- catch (error) {
323
- console.error("[AutoPlay] Failed to fetch spotify recommendations:", error.response?.status, error.response?.data || error.message);
268
+ case Enums_1.AutoPlayPlatform.Spotify: {
269
+ const allowedSpotifyHosts = ["open.spotify.com", "www.spotify.com"];
270
+ if (!allowedSpotifyHosts.includes(parsedURL.host)) {
271
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Spotify, requester);
272
+ if (!resolvedTrack)
324
273
  return [];
325
- }
326
- if (!json.tracks || !json.tracks.length) {
327
- return [];
328
- }
329
- const recommendedTrackId = json.tracks[Math.floor(Math.random() * json.tracks.length)].id;
330
- const res = await this.manager.search({ query: `https://open.spotify.com/track/${recommendedTrackId}`, source: Manager_1.SearchPlatform.Spotify }, track.requester);
331
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
332
- return [];
333
- }
334
- if (res.loadType === LoadTypes.Playlist) {
335
- res.tracks = res.playlist.tracks;
336
- }
337
- if (!res.tracks.length) {
338
- return [];
339
- }
340
- return res.tracks;
341
- }
342
- catch (error) {
343
- console.error("[AutoPlay] Unexpected spotify error:", error.message || error);
344
- return [];
274
+ track = resolvedTrack;
345
275
  }
346
- break;
347
- case "deezer":
348
- if (!track.uri.includes("deezer")) {
349
- const res = await this.manager.search({ query: `${track.author} - ${track.title}`, source: Manager_1.SearchPlatform.Deezer }, track.requester);
350
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
351
- return [];
352
- }
353
- if (res.loadType === LoadTypes.Playlist) {
354
- res.tracks = res.playlist.tracks;
355
- }
356
- if (!res.tracks.length) {
276
+ const extractSpotifyArtistID = (url) => {
277
+ const regex = /https:\/\/open\.spotify\.com\/artist\/([a-zA-Z0-9]+)/;
278
+ const match = url.match(regex);
279
+ return match ? match[1] : null;
280
+ };
281
+ const identifier = `sprec:seed_artists=${extractSpotifyArtistID(track.pluginInfo.artistUrl)}&seed_tracks=${track.identifier}`;
282
+ const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
283
+ const tracks = this.buildTracksFromResponse(recommendedResult, requester);
284
+ return tracks;
285
+ }
286
+ case Enums_1.AutoPlayPlatform.Deezer: {
287
+ const allowedDeezerHosts = ["deezer.com", "www.deezer.com", "www.deezer.page.link"];
288
+ if (!allowedDeezerHosts.includes(parsedURL.host)) {
289
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Deezer, requester);
290
+ if (!resolvedTrack)
357
291
  return [];
358
- }
359
- track = res.tracks[0];
292
+ track = resolvedTrack;
360
293
  }
361
294
  const identifier = `dzrec:${track.identifier}`;
362
295
  const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
363
- if (!recommendedResult) {
364
- return [];
365
- }
366
- let tracks = [];
367
- let playlist = null;
368
- const requester = track.requester;
369
- switch (recommendedResult.loadType) {
370
- case LoadTypes.Search:
371
- tracks = recommendedResult.data.map((track) => TrackUtils.build(track, requester));
372
- break;
373
- case LoadTypes.Track:
374
- tracks = [TrackUtils.build(recommendedResult.data, requester)];
375
- break;
376
- case LoadTypes.Playlist: {
377
- const playlistData = recommendedResult.data;
378
- tracks = playlistData.tracks.map((track) => TrackUtils.build(track, requester));
379
- playlist = {
380
- name: playlistData.info.name,
381
- playlistInfo: playlistData.pluginInfo,
382
- requester: requester,
383
- tracks,
384
- duration: tracks.reduce((acc, cur) => acc + (cur.duration || 0), 0),
385
- };
386
- break;
387
- }
388
- }
389
- const result = { loadType: recommendedResult.loadType, tracks, playlist };
390
- if (result.loadType === LoadTypes.Empty || result.loadType === LoadTypes.Error) {
391
- return [];
392
- }
393
- if (result.loadType === LoadTypes.Playlist) {
394
- result.tracks = result.playlist.tracks;
395
- }
396
- if (!result.tracks.length) {
397
- return [];
398
- }
399
- return result.tracks;
400
- break;
401
- case "soundcloud":
402
- if (!track.uri.includes("soundcloud")) {
403
- const res = await this.manager.search({ query: `${track.author} - ${track.title}`, source: Manager_1.SearchPlatform.SoundCloud }, track.requester);
404
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
405
- return [];
406
- }
407
- if (res.loadType === LoadTypes.Playlist) {
408
- res.tracks = res.playlist.tracks;
409
- }
410
- if (!res.tracks.length) {
296
+ const tracks = this.buildTracksFromResponse(recommendedResult, requester);
297
+ return tracks;
298
+ }
299
+ case Enums_1.AutoPlayPlatform.SoundCloud: {
300
+ const allowedSoundCloudHosts = ["soundcloud.com", "www.soundcloud.com"];
301
+ if (!allowedSoundCloudHosts.includes(parsedURL.host)) {
302
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.SoundCloud, requester);
303
+ if (!resolvedTrack)
411
304
  return [];
412
- }
413
- track = res.tracks[0];
305
+ track = resolvedTrack;
414
306
  }
415
307
  try {
416
308
  const recommendedRes = await axios_1.default.get(`${track.uri}/recommended`).catch((err) => {
@@ -422,50 +314,52 @@ class AutoPlayUtils {
422
314
  }
423
315
  const html = recommendedRes.data;
424
316
  const dom = new jsdom_1.JSDOM(html);
425
- const document = dom.window.document;
426
- const secondNoscript = document.querySelectorAll("noscript")[1];
317
+ const window = dom.window;
318
+ // Narrow the element types using instanceof
319
+ const secondNoscript = window.querySelectorAll("noscript")[1];
320
+ if (!secondNoscript || !(secondNoscript instanceof window.Element))
321
+ return [];
427
322
  const sectionElement = secondNoscript.querySelector("section");
323
+ if (!sectionElement || !(sectionElement instanceof window.HTMLElement))
324
+ return [];
428
325
  const articleElements = sectionElement.querySelectorAll("article");
429
- if (!articleElements || articleElements.length === 0) {
326
+ if (!articleElements || articleElements.length === 0)
430
327
  return [];
431
- }
432
328
  const urls = Array.from(articleElements)
433
- .map((articleElement) => {
434
- const h2Element = articleElement.querySelector('h2[itemprop="name"]');
435
- const aElement = h2Element?.querySelector('a[itemprop="url"]');
436
- return aElement ? `https://soundcloud.com${aElement.getAttribute("href")}` : null;
329
+ .map((element) => {
330
+ const h2 = element.querySelector('h2[itemprop="name"]');
331
+ if (!h2)
332
+ return null;
333
+ const a = h2.querySelector('a[itemprop="url"]');
334
+ if (!a)
335
+ return null;
336
+ const href = a.getAttribute("href");
337
+ return href ? `https://soundcloud.com${href}` : null;
437
338
  })
438
339
  .filter(Boolean);
439
- if (!urls.length) {
340
+ if (!urls.length)
440
341
  return [];
441
- }
442
342
  const randomUrl = urls[Math.floor(Math.random() * urls.length)];
443
- const res = await this.manager.search({ query: randomUrl, source: Manager_1.SearchPlatform.SoundCloud }, track.requester);
444
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
445
- return [];
446
- }
447
- if (res.loadType === LoadTypes.Playlist) {
448
- res.tracks = res.playlist.tracks;
449
- }
450
- if (!res.tracks.length) {
451
- return [];
452
- }
453
- return res.tracks;
343
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(randomUrl, Enums_1.SearchPlatform.SoundCloud, requester);
344
+ return resolvedTrack ? [resolvedTrack] : [];
454
345
  }
455
346
  catch (error) {
456
347
  console.error("[AutoPlay] Error occurred while fetching soundcloud recommendations:", error);
457
348
  return [];
458
349
  }
459
- break;
460
- case "youtube":
461
- const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => track.uri.includes(url));
350
+ }
351
+ case Enums_1.AutoPlayPlatform.YouTube: {
352
+ const allowedYouTubeHosts = ["youtube.com", "youtu.be"];
353
+ const hasYouTubeURL = allowedYouTubeHosts.some((url) => track.uri.includes(url));
462
354
  let videoID = null;
463
355
  if (hasYouTubeURL) {
464
356
  videoID = track.uri.split("=").pop();
465
357
  }
466
358
  else {
467
- const searchResult = await this.manager.search({ query: `${track.author} - ${track.title}`, source: Manager_1.SearchPlatform.YouTube }, track.requester);
468
- videoID = searchResult.tracks[0]?.uri.split("=").pop();
359
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.YouTube, requester);
360
+ if (!resolvedTrack)
361
+ return [];
362
+ videoID = resolvedTrack.uri.split("=").pop();
469
363
  }
470
364
  if (!videoID) {
471
365
  return [];
@@ -476,16 +370,213 @@ class AutoPlayUtils {
476
370
  randomIndex = Math.floor(Math.random() * 23) + 2;
477
371
  searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`;
478
372
  } while (track.uri.includes(searchURI));
479
- const res = await this.manager.search({ query: searchURI, source: Manager_1.SearchPlatform.YouTube }, track.requester);
480
- if (res.loadType === LoadTypes.Empty || res.loadType === LoadTypes.Error) {
481
- return [];
482
- }
483
- const filteredTracks = res.tracks.filter((t) => t.uri !== track.uri);
373
+ const resolvedTracks = await this.resolveTracksFromQuery(searchURI, Enums_1.SearchPlatform.YouTube, requester);
374
+ const filteredTracks = resolvedTracks.filter((t) => t.uri !== track.uri);
484
375
  return filteredTracks;
376
+ }
377
+ case Enums_1.AutoPlayPlatform.Tidal: {
378
+ const allowedTidalHosts = ["tidal.com", "www.tidal.com"];
379
+ if (!allowedTidalHosts.includes(parsedURL.host)) {
380
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Tidal, requester);
381
+ if (!resolvedTrack)
382
+ return [];
383
+ track = resolvedTrack;
384
+ }
385
+ const identifier = `tdrec:${track.identifier}`;
386
+ const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
387
+ const tracks = this.buildTracksFromResponse(recommendedResult, requester);
388
+ return tracks;
389
+ }
390
+ case Enums_1.AutoPlayPlatform.VKMusic: {
391
+ const allowedVKHosts = ["vk.com", "www.vk.com", "vk.ru", "www.vk.ru"];
392
+ if (!allowedVKHosts.includes(parsedURL.host)) {
393
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.VKMusic, requester);
394
+ if (!resolvedTrack)
395
+ return [];
396
+ track = resolvedTrack;
397
+ }
398
+ const identifier = `vkrec:${track.identifier}`;
399
+ const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
400
+ const tracks = this.buildTracksFromResponse(recommendedResult, requester);
401
+ return tracks;
402
+ }
403
+ case Enums_1.AutoPlayPlatform.Qobuz: {
404
+ const allowedQobuzHosts = ["qobuz.com", "www.qobuz.com", "play.qobuz.com"];
405
+ if (!allowedQobuzHosts.includes(parsedURL.host)) {
406
+ const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Qobuz, requester);
407
+ if (!resolvedTrack)
408
+ return [];
409
+ track = resolvedTrack;
410
+ }
411
+ const identifier = `qbrec:${track.identifier}`;
412
+ const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
413
+ const tracks = this.buildTracksFromResponse(recommendedResult, requester);
414
+ return tracks;
415
+ }
485
416
  default:
486
417
  return [];
487
418
  }
488
419
  }
420
+ /**
421
+ * Searches for a track using the manager and returns resolved tracks.
422
+ * @param query The search query (artist - title).
423
+ * @param requester The requester who initiated the search.
424
+ * @returns An array of resolved tracks, or an empty array if not found or error occurred.
425
+ */
426
+ static async resolveTracksFromQuery(query, source, requester) {
427
+ try {
428
+ const searchResult = await this.manager.search({ query, source }, requester);
429
+ if (TrackUtils.isErrorOrEmptySearchResult(searchResult)) {
430
+ return [];
431
+ }
432
+ switch (searchResult.loadType) {
433
+ case Enums_1.LoadTypes.Album:
434
+ case Enums_1.LoadTypes.Artist:
435
+ case Enums_1.LoadTypes.Station:
436
+ case Enums_1.LoadTypes.Podcast:
437
+ case Enums_1.LoadTypes.Show:
438
+ case Enums_1.LoadTypes.Playlist:
439
+ return searchResult.playlist.tracks;
440
+ case Enums_1.LoadTypes.Track:
441
+ case Enums_1.LoadTypes.Search:
442
+ case Enums_1.LoadTypes.Short:
443
+ return searchResult.tracks;
444
+ default:
445
+ return [];
446
+ }
447
+ }
448
+ catch (error) {
449
+ console.error("[TrackResolver] Failed to resolve query:", query, error);
450
+ return [];
451
+ }
452
+ }
453
+ /**
454
+ * Resolves the first available track from a search query using the specified source.
455
+ * Useful for normalizing tracks that lack platform-specific metadata or URIs.
456
+ *
457
+ * @param query - The search query string (usually "Artist - Title").
458
+ * @param source - The search platform to use (e.g., Spotify, Deezer, YouTube).
459
+ * @param requester - The requester object, used for context or attribution.
460
+ * @returns A single resolved {@link Track} object if found, or `null` if the search fails or returns no results.
461
+ */
462
+ static async resolveFirstTrackFromQuery(query, source, requester) {
463
+ try {
464
+ const searchResult = await this.manager.search({ query, source }, requester);
465
+ if (TrackUtils.isErrorOrEmptySearchResult(searchResult))
466
+ return null;
467
+ switch (searchResult.loadType) {
468
+ case Enums_1.LoadTypes.Album:
469
+ case Enums_1.LoadTypes.Artist:
470
+ case Enums_1.LoadTypes.Station:
471
+ case Enums_1.LoadTypes.Podcast:
472
+ case Enums_1.LoadTypes.Show:
473
+ case Enums_1.LoadTypes.Playlist:
474
+ return searchResult.playlist.tracks[0] || null;
475
+ case Enums_1.LoadTypes.Track:
476
+ case Enums_1.LoadTypes.Search:
477
+ case Enums_1.LoadTypes.Short:
478
+ return searchResult.tracks[0] || null;
479
+ default:
480
+ return null;
481
+ }
482
+ }
483
+ catch (err) {
484
+ console.error(`[AutoPlay] Failed to resolve track from query: "${query}" on source: ${source}`, err);
485
+ return null;
486
+ }
487
+ }
488
+ // static async getSpotifyAccessToken() {
489
+ // const timeoutMs = 15000;
490
+ // let browser;
491
+ // let timeout;
492
+ // try {
493
+ // browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"] });
494
+ // const page = await browser.newPage();
495
+ // let tokenCaptured = false;
496
+ // timeout = setTimeout(async () => {
497
+ // if (!tokenCaptured) {
498
+ // console.warn("[Spotify] Token request timeout — did Spotify change their internals?");
499
+ // await browser?.close();
500
+ // }
501
+ // }, timeoutMs);
502
+ // page.on("requestfinished", async (request) => {
503
+ // if (!request.url().includes("/api/token")) return;
504
+ // tokenCaptured = true;
505
+ // try {
506
+ // const response = await request.response();
507
+ // if (response && response.ok()) {
508
+ // const data = await response.json();
509
+ // this.cachedAccessToken = data?.accessToken ?? null;
510
+ // this.cachedAccessTokenExpiresAt = data?.accessTokenExpirationTimestampMs ?? 0;
511
+ // }
512
+ // } catch (err) {
513
+ // console.error("[Spotify] Error reading token response:", err);
514
+ // }
515
+ // clearTimeout(timeout);
516
+ // page.removeAllListeners();
517
+ // await browser.close();
518
+ // });
519
+ // try {
520
+ // await page.goto("https://open.spotify.com/", { waitUntil: "domcontentloaded" });
521
+ // } catch (err) {
522
+ // clearTimeout(timeout);
523
+ // await browser.close();
524
+ // console.error("[Spotify] Failed to navigate:", err);
525
+ // return [];
526
+ // }
527
+ // } catch (err) {
528
+ // clearTimeout(timeout);
529
+ // await browser?.close();
530
+ // console.error("[Spotify] Failed to launch Playwright:", err);
531
+ // }
532
+ // }
533
+ static isPlaylistRawData(data) {
534
+ return typeof data === "object" && data !== null && Array.isArray(data.tracks);
535
+ }
536
+ static isTrackData(data) {
537
+ return typeof data === "object" && data !== null && "encoded" in data && "info" in data;
538
+ }
539
+ static isTrackDataArray(data) {
540
+ return (Array.isArray(data) &&
541
+ data.every((track) => typeof track === "object" && track !== null && "encoded" in track && "info" in track && typeof track.encoded === "string"));
542
+ }
543
+ static buildTracksFromResponse(recommendedResult, requester) {
544
+ if (!recommendedResult)
545
+ return [];
546
+ if (TrackUtils.isErrorOrEmptySearchResult(recommendedResult))
547
+ return [];
548
+ switch (recommendedResult.loadType) {
549
+ case Enums_1.LoadTypes.Track: {
550
+ const data = recommendedResult.data;
551
+ if (!this.isTrackData(data)) {
552
+ throw new Error("[TrackBuilder] Invalid TrackData object.");
553
+ }
554
+ return [TrackUtils.build(data, requester)];
555
+ }
556
+ case Enums_1.LoadTypes.Short:
557
+ case Enums_1.LoadTypes.Search: {
558
+ const data = recommendedResult.data;
559
+ if (!this.isTrackDataArray(data)) {
560
+ throw new Error("[TrackBuilder] Invalid TrackData[] array for LoadTypes.Search or Short.");
561
+ }
562
+ return data.map((d) => TrackUtils.build(d, requester));
563
+ }
564
+ case Enums_1.LoadTypes.Album:
565
+ case Enums_1.LoadTypes.Artist:
566
+ case Enums_1.LoadTypes.Station:
567
+ case Enums_1.LoadTypes.Podcast:
568
+ case Enums_1.LoadTypes.Show:
569
+ case Enums_1.LoadTypes.Playlist: {
570
+ const data = recommendedResult.data;
571
+ if (this.isPlaylistRawData(data)) {
572
+ return data.tracks.map((d) => TrackUtils.build(d, requester));
573
+ }
574
+ throw new Error(`[TrackBuilder] Invalid playlist data for loadType: ${recommendedResult.loadType}`);
575
+ }
576
+ default:
577
+ throw new Error(`[TrackBuilder] Unsupported loadType: ${recommendedResult.loadType}`);
578
+ }
579
+ }
489
580
  }
490
581
  exports.AutoPlayUtils = AutoPlayUtils;
491
582
  /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
@@ -516,7 +607,7 @@ class Structure {
516
607
  exports.Structure = Structure;
517
608
  const structures = {
518
609
  Player: require("./Player").Player,
519
- Queue: require("./Queue").Queue,
610
+ Queue: require("../statestorage/MemoryQueue").MemoryQueue,
520
611
  Node: require("./Node").Node,
521
612
  Filters: require("./Filters").Filters,
522
613
  Manager: require("./Manager").Manager,
@@ -524,45 +615,3 @@ const structures = {
524
615
  Rest: require("./Rest").Rest,
525
616
  Utils: require("./Utils"),
526
617
  };
527
- var LoadTypes;
528
- (function (LoadTypes) {
529
- LoadTypes["Track"] = "track";
530
- LoadTypes["Playlist"] = "playlist";
531
- LoadTypes["Search"] = "search";
532
- LoadTypes["Empty"] = "empty";
533
- LoadTypes["Error"] = "error";
534
- })(LoadTypes || (exports.LoadTypes = LoadTypes = {}));
535
- var StateTypes;
536
- (function (StateTypes) {
537
- StateTypes["Connected"] = "CONNECTED";
538
- StateTypes["Connecting"] = "CONNECTING";
539
- StateTypes["Disconnected"] = "DISCONNECTED";
540
- StateTypes["Disconnecting"] = "DISCONNECTING";
541
- StateTypes["Destroying"] = "DESTROYING";
542
- })(StateTypes || (exports.StateTypes = StateTypes = {}));
543
- var TrackEndReasonTypes;
544
- (function (TrackEndReasonTypes) {
545
- TrackEndReasonTypes["Finished"] = "finished";
546
- TrackEndReasonTypes["LoadFailed"] = "loadFailed";
547
- TrackEndReasonTypes["Stopped"] = "stopped";
548
- TrackEndReasonTypes["Replaced"] = "replaced";
549
- TrackEndReasonTypes["Cleanup"] = "cleanup";
550
- })(TrackEndReasonTypes || (exports.TrackEndReasonTypes = TrackEndReasonTypes = {}));
551
- var SeverityTypes;
552
- (function (SeverityTypes) {
553
- SeverityTypes["Common"] = "common";
554
- SeverityTypes["Suspicious"] = "suspicious";
555
- SeverityTypes["Fault"] = "fault";
556
- })(SeverityTypes || (exports.SeverityTypes = SeverityTypes = {}));
557
- var TrackSourceTypes;
558
- (function (TrackSourceTypes) {
559
- TrackSourceTypes["AppleMusic"] = "applemusic";
560
- TrackSourceTypes["Bandcamp"] = "bandcamp";
561
- TrackSourceTypes["Deezer"] = "deezer";
562
- TrackSourceTypes["Jiosaavn"] = "jiosaavn";
563
- TrackSourceTypes["SoundCloud"] = "soundcloud";
564
- TrackSourceTypes["Spotify"] = "spotify";
565
- TrackSourceTypes["Tidal"] = "tidal";
566
- TrackSourceTypes["VKMusic"] = "vkmusic";
567
- TrackSourceTypes["YouTube"] = "youtube";
568
- })(TrackSourceTypes || (exports.TrackSourceTypes = TrackSourceTypes = {}));