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.
- package/README.md +2 -2
- package/dist/index.d.ts +2275 -959
- package/dist/index.js +14 -1
- package/dist/statestorage/JsonQueue.js +436 -0
- package/dist/{structures/Queue.js → statestorage/MemoryQueue.js} +205 -78
- package/dist/statestorage/RedisQueue.js +427 -0
- package/dist/structures/Enums.js +259 -0
- package/dist/structures/Filters.js +54 -82
- package/dist/structures/Manager.js +813 -376
- package/dist/structures/Node.js +348 -203
- package/dist/structures/Player.js +302 -135
- package/dist/structures/Plugin.js +4 -1
- package/dist/structures/Rest.js +11 -7
- package/dist/structures/Types.js +3 -0
- package/dist/structures/Utils.js +312 -263
- package/dist/utils/managerCheck.js +19 -19
- package/dist/utils/nodeCheck.js +5 -5
- package/dist/wrappers/detritus.js +36 -0
- package/dist/wrappers/discord.js.js +29 -0
- package/dist/wrappers/eris.js +29 -0
- package/dist/wrappers/oceanic.js +29 -0
- package/dist/wrappers/seyfert.js +43 -0
- package/package.json +20 -14
package/dist/structures/Utils.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.Structure = exports.AutoPlayUtils = exports.TrackUtils = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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(
|
|
52
|
-
this.trackPartial.unshift(
|
|
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
|
|
194
|
-
if (
|
|
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
|
|
232
|
-
if (
|
|
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
|
|
246
|
-
if (
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
426
|
-
|
|
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((
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
444
|
-
|
|
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
|
-
|
|
460
|
-
case
|
|
461
|
-
const
|
|
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
|
|
468
|
-
|
|
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
|
|
480
|
-
|
|
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("
|
|
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 = {}));
|