aqualink 2.7.3 → 2.8.0
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/build/handlers/fetchImage.js +121 -23
- package/build/index.d.ts +169 -10
- package/build/structures/Aqua.js +240 -317
- package/build/structures/Connection.js +20 -24
- package/build/structures/Node.js +113 -88
- package/build/structures/Player.js +155 -169
- package/build/structures/Queue.js +13 -22
- package/build/structures/Rest.js +107 -136
- package/build/structures/Track.js +113 -33
- package/package.json +1 -1
|
@@ -1,50 +1,148 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
|
|
3
|
+
const sourceHandlers = {
|
|
4
|
+
spotify: fetchSpotifyThumbnail,
|
|
5
|
+
youtube: fetchYouTubeThumbnail
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default'];
|
|
9
|
+
|
|
10
|
+
const YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/;
|
|
11
|
+
const SPOTIFY_URI_REGEX = /^https:\/\/open\.spotify\.com\/(track|album|playlist)\/[a-zA-Z0-9]+/;
|
|
8
12
|
|
|
9
13
|
async function getImageUrl(info) {
|
|
10
14
|
if (!info?.sourceName || !info?.uri) return null;
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
const sourceName = info.sourceName.toLowerCase();
|
|
17
|
+
const handler = sourceHandlers[sourceName];
|
|
18
|
+
|
|
12
19
|
if (!handler) return null;
|
|
20
|
+
|
|
13
21
|
try {
|
|
14
|
-
|
|
22
|
+
const param = sourceName === 'spotify' ? info.uri :
|
|
23
|
+
sourceName === 'youtube' ? extractYouTubeId(info.uri) : info.uri;
|
|
24
|
+
|
|
25
|
+
if (!param) return null;
|
|
26
|
+
|
|
27
|
+
return await handler(param);
|
|
15
28
|
} catch (error) {
|
|
16
|
-
console.error(
|
|
29
|
+
console.error(`Error fetching ${sourceName} thumbnail:`, error.message);
|
|
17
30
|
return null;
|
|
18
31
|
}
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
function
|
|
34
|
+
function extractYouTubeId(uri) {
|
|
35
|
+
if (!uri) return null;
|
|
36
|
+
|
|
37
|
+
let id = null;
|
|
38
|
+
|
|
39
|
+
if (uri.includes('youtube.com/watch?v=')) {
|
|
40
|
+
id = uri.split('v=')[1]?.split('&')[0];
|
|
41
|
+
} else if (uri.includes('youtu.be/')) {
|
|
42
|
+
id = uri.split('youtu.be/')[1]?.split('?')[0];
|
|
43
|
+
} else if (uri.includes('youtube.com/embed/')) {
|
|
44
|
+
id = uri.split('embed/')[1]?.split('?')[0];
|
|
45
|
+
} else if (YOUTUBE_ID_REGEX.test(uri)) {
|
|
46
|
+
id = uri;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return id && YOUTUBE_ID_REGEX.test(id) ? id : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fetchSpotifyThumbnail(uri) {
|
|
53
|
+
if (!SPOTIFY_URI_REGEX.test(uri)) {
|
|
54
|
+
throw new Error('Invalid Spotify URI format');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const url = `https://open.spotify.com/oembed?url=${encodeURIComponent(uri)}`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const data = await fetchJson(url);
|
|
61
|
+
return data?.thumbnail_url || null;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Spotify fetch failed: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function fetchYouTubeThumbnail(identifier) {
|
|
68
|
+
if (!identifier || !YOUTUBE_ID_REGEX.test(identifier)) {
|
|
69
|
+
throw new Error('Invalid YouTube identifier');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const quality of YOUTUBE_QUALITIES) {
|
|
73
|
+
const url = `https://img.youtube.com/vi/${identifier}/${quality}.jpg`;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const exists = await checkImageExists(url);
|
|
77
|
+
if (exists) return url;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fetchJson(url) {
|
|
22
87
|
return new Promise((resolve, reject) => {
|
|
23
|
-
https.get(url, (res) => {
|
|
88
|
+
const request = https.get(url, (res) => {
|
|
24
89
|
if (res.statusCode !== 200) {
|
|
25
90
|
res.resume();
|
|
26
|
-
return reject(`
|
|
91
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
27
92
|
}
|
|
28
|
-
|
|
29
|
-
|
|
93
|
+
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let totalLength = 0;
|
|
96
|
+
|
|
97
|
+
res.on('data', chunk => {
|
|
98
|
+
chunks.push(chunk);
|
|
99
|
+
totalLength += chunk.length;
|
|
100
|
+
|
|
101
|
+
if (totalLength > 1024 * 1024) {
|
|
102
|
+
res.destroy();
|
|
103
|
+
reject(new Error('Response too large'));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
30
107
|
res.on('end', () => {
|
|
31
108
|
try {
|
|
109
|
+
const data = Buffer.concat(chunks, totalLength).toString('utf8');
|
|
32
110
|
const json = JSON.parse(data);
|
|
33
|
-
resolve(json
|
|
111
|
+
resolve(json);
|
|
34
112
|
} catch (error) {
|
|
35
|
-
reject(`JSON parse error: ${error.message}`);
|
|
113
|
+
reject(new Error(`JSON parse error: ${error.message}`));
|
|
36
114
|
}
|
|
37
115
|
});
|
|
38
|
-
})
|
|
39
|
-
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
request.setTimeout(5000, () => {
|
|
119
|
+
request.destroy();
|
|
120
|
+
reject(new Error('Request timeout'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
request.on('error', (error) => {
|
|
124
|
+
reject(new Error(`Request error: ${error.message}`));
|
|
40
125
|
});
|
|
41
126
|
});
|
|
42
127
|
}
|
|
43
128
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
129
|
+
function checkImageExists(url) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const request = https.request(url, { method: 'HEAD' }, (res) => {
|
|
132
|
+
resolve(res.statusCode === 200);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
request.setTimeout(3000, () => {
|
|
136
|
+
request.destroy();
|
|
137
|
+
resolve(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
request.on('error', () => resolve(false));
|
|
141
|
+
request.end();
|
|
142
|
+
});
|
|
48
143
|
}
|
|
49
144
|
|
|
50
|
-
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
getImageUrl,
|
|
148
|
+
};
|
package/build/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare module "aqualink" {
|
|
|
19
19
|
autoResume: boolean;
|
|
20
20
|
infiniteReconnects: boolean;
|
|
21
21
|
options: AquaOptions;
|
|
22
|
+
failoverOptions: FailoverOptions;
|
|
22
23
|
_leastUsedCache: { nodes: Node[], timestamp: number };
|
|
23
24
|
|
|
24
25
|
defaultSendFunction(payload: any): void;
|
|
@@ -39,8 +40,13 @@ declare module "aqualink" {
|
|
|
39
40
|
handleNoMatches(rest: Rest, query: string): Promise<any>;
|
|
40
41
|
constructResponse(response: any, requester: any, requestNode: Node): ResolveResponse;
|
|
41
42
|
get(guildId: string): Player;
|
|
42
|
-
search(query: string, requester: any, source?:
|
|
43
|
+
search(query: string, requester: any, source?: SearchSource): Promise<Track[] | null>;
|
|
44
|
+
searchSuggestions(query: string, source?: SearchSource): Promise<SearchSuggestion[]>;
|
|
45
|
+
autocomplete(query: string, source?: SearchSource): Promise<AutocompleteResult>;
|
|
43
46
|
cleanupPlayer(player: Player): Promise<void>;
|
|
47
|
+
handleFailover(player: Player, error: Error): Promise<boolean>;
|
|
48
|
+
getHealthyNodes(): Node[];
|
|
49
|
+
isNodeHealthy(node: Node): boolean;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export class Node {
|
|
@@ -66,11 +72,17 @@ declare module "aqualink" {
|
|
|
66
72
|
reconnectAttempted: number;
|
|
67
73
|
reconnectTimeoutId: NodeJS.Timeout | null;
|
|
68
74
|
stats: NodeStats;
|
|
75
|
+
lastFailure: number;
|
|
76
|
+
health: NodeHealth;
|
|
69
77
|
|
|
70
78
|
initializeStats(): void;
|
|
71
79
|
connect(): Promise<void>;
|
|
72
80
|
destroy(clean?: boolean): void;
|
|
73
81
|
getStats(): Promise<NodeStats>;
|
|
82
|
+
isHealthy(): boolean;
|
|
83
|
+
markFailure(): void;
|
|
84
|
+
markSuccess(): void;
|
|
85
|
+
getHealth(): NodeHealth;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
export class Player extends EventEmitter {
|
|
@@ -83,7 +95,7 @@ declare module "aqualink" {
|
|
|
83
95
|
connection: Connection;
|
|
84
96
|
filters: Filters;
|
|
85
97
|
volume: number;
|
|
86
|
-
loop:
|
|
98
|
+
loop: LoopMode;
|
|
87
99
|
queue: Queue;
|
|
88
100
|
previousTracks: Track[];
|
|
89
101
|
previousTracksIndex: number;
|
|
@@ -100,6 +112,8 @@ declare module "aqualink" {
|
|
|
100
112
|
nowPlayingMessage: any;
|
|
101
113
|
isAutoplayEnabled: boolean;
|
|
102
114
|
isAutoplay: boolean;
|
|
115
|
+
failoverAttempts: number;
|
|
116
|
+
lastFailoverTime: number;
|
|
103
117
|
|
|
104
118
|
play(): Promise<void>;
|
|
105
119
|
connect(options: ConnectionOptions): Player;
|
|
@@ -108,7 +122,7 @@ declare module "aqualink" {
|
|
|
108
122
|
seek(position: number): Player;
|
|
109
123
|
stop(): Player;
|
|
110
124
|
setVolume(volume: number): Player;
|
|
111
|
-
setLoop(mode:
|
|
125
|
+
setLoop(mode: LoopMode): Player;
|
|
112
126
|
setTextChannel(channel: string): Player;
|
|
113
127
|
setVoiceChannel(channel: string): Player;
|
|
114
128
|
disconnect(): Player;
|
|
@@ -119,6 +133,8 @@ declare module "aqualink" {
|
|
|
119
133
|
searchLyrics(query: string): Promise<any>;
|
|
120
134
|
lyrics(): Promise<any>;
|
|
121
135
|
updatePlayer(data: any): Promise<void>;
|
|
136
|
+
switchNode(newNode: Node, preserveState?: boolean): Promise<boolean>;
|
|
137
|
+
canFailover(): boolean;
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
export class Track {
|
|
@@ -157,6 +173,8 @@ declare module "aqualink" {
|
|
|
157
173
|
getRoutePlannerStatus(): Promise<any>;
|
|
158
174
|
getRoutePlannerAddress(address: string): Promise<any>;
|
|
159
175
|
getLyrics(options: { track: Track }): Promise<any>;
|
|
176
|
+
getSearchSuggestions(query: string, source?: SearchSource): Promise<SearchSuggestion[]>;
|
|
177
|
+
getAutocompleteSuggestions(query: string, source?: SearchSource): Promise<AutocompleteResult>;
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
export class Queue extends Array<any> {
|
|
@@ -240,15 +258,30 @@ declare module "aqualink" {
|
|
|
240
258
|
setStateUpdate(data: any): void;
|
|
241
259
|
}
|
|
242
260
|
|
|
261
|
+
// Enhanced interfaces with autocomplete and failover support
|
|
243
262
|
interface AquaOptions {
|
|
244
263
|
shouldDeleteMessage?: boolean;
|
|
245
|
-
defaultSearchPlatform?:
|
|
264
|
+
defaultSearchPlatform?: SearchSource;
|
|
246
265
|
leaveOnEnd?: boolean;
|
|
247
|
-
restVersion?:
|
|
266
|
+
restVersion?: RestVersion;
|
|
248
267
|
plugins?: Plugin[];
|
|
249
268
|
send?: (payload: any) => void;
|
|
250
269
|
autoResume?: boolean;
|
|
251
270
|
infiniteReconnects?: boolean;
|
|
271
|
+
failoverOptions?: FailoverOptions;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
interface FailoverOptions {
|
|
275
|
+
enabled?: boolean;
|
|
276
|
+
maxRetries?: number;
|
|
277
|
+
retryDelay?: number;
|
|
278
|
+
preservePosition?: boolean;
|
|
279
|
+
resumePlayback?: boolean;
|
|
280
|
+
cooldownTime?: number;
|
|
281
|
+
maxFailoverAttempts?: number;
|
|
282
|
+
healthCheckInterval?: number;
|
|
283
|
+
unhealthyThreshold?: number;
|
|
284
|
+
recoveryCooldown?: number;
|
|
252
285
|
}
|
|
253
286
|
|
|
254
287
|
interface NodeOptions {
|
|
@@ -259,6 +292,9 @@ declare module "aqualink" {
|
|
|
259
292
|
secure?: boolean;
|
|
260
293
|
sessionId?: string;
|
|
261
294
|
regions?: string[];
|
|
295
|
+
priority?: number;
|
|
296
|
+
retryAmount?: number;
|
|
297
|
+
retryDelay?: number;
|
|
262
298
|
}
|
|
263
299
|
|
|
264
300
|
interface NodeAdditionalOptions {
|
|
@@ -274,9 +310,11 @@ declare module "aqualink" {
|
|
|
274
310
|
textChannel: string;
|
|
275
311
|
voiceChannel: string;
|
|
276
312
|
defaultVolume?: number;
|
|
277
|
-
loop?:
|
|
313
|
+
loop?: LoopMode;
|
|
278
314
|
shouldDeleteMessage?: boolean;
|
|
279
315
|
leaveOnEnd?: boolean;
|
|
316
|
+
autoplay?: boolean;
|
|
317
|
+
enableFailover?: boolean;
|
|
280
318
|
}
|
|
281
319
|
|
|
282
320
|
interface ConnectionOptions {
|
|
@@ -288,15 +326,15 @@ declare module "aqualink" {
|
|
|
288
326
|
|
|
289
327
|
interface ResolveOptions {
|
|
290
328
|
query: string;
|
|
291
|
-
source?:
|
|
329
|
+
source?: SearchSource;
|
|
292
330
|
requester: any;
|
|
293
331
|
nodes?: string | Node;
|
|
294
332
|
}
|
|
295
333
|
|
|
296
334
|
interface ResolveResponse {
|
|
297
|
-
loadType:
|
|
298
|
-
exception:
|
|
299
|
-
playlistInfo:
|
|
335
|
+
loadType: LoadType;
|
|
336
|
+
exception: LavalinkException | null;
|
|
337
|
+
playlistInfo: PlaylistInfo | null;
|
|
300
338
|
pluginInfo: any;
|
|
301
339
|
tracks: Track[];
|
|
302
340
|
}
|
|
@@ -346,6 +384,7 @@ declare module "aqualink" {
|
|
|
346
384
|
uri: string;
|
|
347
385
|
sourceName: string;
|
|
348
386
|
artworkUrl: string;
|
|
387
|
+
position?: number;
|
|
349
388
|
}
|
|
350
389
|
|
|
351
390
|
interface FilterOptions {
|
|
@@ -365,4 +404,124 @@ declare module "aqualink" {
|
|
|
365
404
|
vaporwave?: any;
|
|
366
405
|
_8d?: any;
|
|
367
406
|
}
|
|
407
|
+
|
|
408
|
+
interface SearchSuggestion {
|
|
409
|
+
text: string;
|
|
410
|
+
highlighted: string;
|
|
411
|
+
type: SuggestionType;
|
|
412
|
+
source: SearchSource;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
interface AutocompleteResult {
|
|
416
|
+
query: string;
|
|
417
|
+
suggestions: SearchSuggestion[];
|
|
418
|
+
hasMore: boolean;
|
|
419
|
+
timestamp: number;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface PlaylistInfo {
|
|
423
|
+
name: string;
|
|
424
|
+
selectedTrack: number;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
interface LavalinkException {
|
|
428
|
+
message: string;
|
|
429
|
+
severity: ExceptionSeverity;
|
|
430
|
+
cause: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface NodeHealth {
|
|
434
|
+
healthy: boolean;
|
|
435
|
+
consecutiveFailures: number;
|
|
436
|
+
lastCheck: number;
|
|
437
|
+
responseTime: number;
|
|
438
|
+
uptime: number;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
type SearchSource =
|
|
442
|
+
| 'ytsearch'
|
|
443
|
+
| 'ytmsearch'
|
|
444
|
+
| 'scsearch'
|
|
445
|
+
| 'spsearch'
|
|
446
|
+
| 'amsearch'
|
|
447
|
+
| 'dzsearch'
|
|
448
|
+
| 'yandexsearch'
|
|
449
|
+
| 'soundcloud'
|
|
450
|
+
| 'youtube'
|
|
451
|
+
| 'spotify'
|
|
452
|
+
| 'applemusic'
|
|
453
|
+
| 'deezer'
|
|
454
|
+
| 'bandcamp'
|
|
455
|
+
| 'vimeo'
|
|
456
|
+
| 'twitch'
|
|
457
|
+
| 'http';
|
|
458
|
+
|
|
459
|
+
type LoopMode = 'none' | 'track' | 'queue';
|
|
460
|
+
|
|
461
|
+
type LoadType =
|
|
462
|
+
| 'track'
|
|
463
|
+
| 'playlist'
|
|
464
|
+
| 'search'
|
|
465
|
+
| 'empty'
|
|
466
|
+
| 'error';
|
|
467
|
+
|
|
468
|
+
type RestVersion = 'v3' | 'v4';
|
|
469
|
+
|
|
470
|
+
type SuggestionType =
|
|
471
|
+
| 'track'
|
|
472
|
+
| 'artist'
|
|
473
|
+
| 'album'
|
|
474
|
+
| 'playlist'
|
|
475
|
+
| 'query';
|
|
476
|
+
|
|
477
|
+
type ExceptionSeverity =
|
|
478
|
+
| 'common'
|
|
479
|
+
| 'suspicious'
|
|
480
|
+
| 'fault';
|
|
481
|
+
|
|
482
|
+
interface AquaEvents {
|
|
483
|
+
'nodeConnect': (node: Node) => void;
|
|
484
|
+
'nodeDisconnect': (node: Node, code: number, reason: string) => void;
|
|
485
|
+
'nodeError': (node: Node, error: Error) => void;
|
|
486
|
+
'nodeReconnect': (node: Node) => void;
|
|
487
|
+
'playerCreate': (player: Player) => void;
|
|
488
|
+
'playerDestroy': (player: Player) => void;
|
|
489
|
+
'trackStart': (player: Player, track: Track) => void;
|
|
490
|
+
'trackEnd': (player: Player, track: Track) => void;
|
|
491
|
+
'trackError': (player: Player, track: Track, error: Error) => void;
|
|
492
|
+
'trackStuck': (player: Player, track: Track, thresholdMs: number) => void;
|
|
493
|
+
'queueEnd': (player: Player) => void;
|
|
494
|
+
'playerMove': (player: Player, oldChannel: string, newChannel: string) => void;
|
|
495
|
+
'playerDisconnect': (player: Player, oldChannel: string) => void;
|
|
496
|
+
'failover': (player: Player, oldNode: Node, newNode: Node) => void;
|
|
497
|
+
'failoverFailed': (player: Player, error: Error) => void;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
interface PlayerEvents {
|
|
501
|
+
'trackStart': (track: Track) => void;
|
|
502
|
+
'trackEnd': (track: Track) => void;
|
|
503
|
+
'trackError': (track: Track, error: Error) => void;
|
|
504
|
+
'trackStuck': (track: Track, thresholdMs: number) => void;
|
|
505
|
+
'playerUpdate': (state: any) => void;
|
|
506
|
+
'queueEnd': () => void;
|
|
507
|
+
'socketClosed': (code: number, reason: string, byRemote: boolean) => void;
|
|
508
|
+
'failover': (oldNode: Node, newNode: Node) => void;
|
|
509
|
+
'failoverFailed': (error: Error) => void;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const DEFAULT_OPTIONS: Required<AquaOptions>;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
declare module "aqualink" {
|
|
516
|
+
interface Aqua {
|
|
517
|
+
on<K extends keyof AquaEvents>(event: K, listener: AquaEvents[K]): this;
|
|
518
|
+
once<K extends keyof AquaEvents>(event: K, listener: AquaEvents[K]): this;
|
|
519
|
+
emit<K extends keyof AquaEvents>(event: K, ...args: Parameters<AquaEvents[K]>): boolean;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
interface Player {
|
|
523
|
+
on<K extends keyof PlayerEvents>(event: K, listener: PlayerEvents[K]): this;
|
|
524
|
+
once<K extends keyof PlayerEvents>(event: K, listener: PlayerEvents[K]): this;
|
|
525
|
+
emit<K extends keyof PlayerEvents>(event: K, ...args: Parameters<PlayerEvents[K]>): boolean;
|
|
526
|
+
}
|
|
368
527
|
}
|