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.
@@ -1,50 +1,148 @@
1
1
  const https = require('https');
2
- const sourceHandlers = new Map([
3
- ['spotify', uri => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
4
- ['youtube', identifier => fetchYouTubeThumbnail(identifier)]
5
- ]);
6
- const YOUTUBE_URL_TEMPLATE = (quality) => (id) => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
7
- const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default'].map(YOUTUBE_URL_TEMPLATE);
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
- const handler = sourceHandlers.get(info.sourceName.toLowerCase());
15
+
16
+ const sourceName = info.sourceName.toLowerCase();
17
+ const handler = sourceHandlers[sourceName];
18
+
12
19
  if (!handler) return null;
20
+
13
21
  try {
14
- return await handler(info.uri);
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('Error fetching image URL:', error);
29
+ console.error(`Error fetching ${sourceName} thumbnail:`, error.message);
17
30
  return null;
18
31
  }
19
32
  }
20
33
 
21
- function fetchThumbnail(url) {
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(`Failed to fetch: ${res.statusCode}`);
91
+ return reject(new Error(`HTTP ${res.statusCode}`));
27
92
  }
28
- let data = '';
29
- res.on('data', chunk => data += chunk);
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.thumbnail_url || null);
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
- }).on('error', (error) => {
39
- reject(`Request error: ${error.message}`);
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
- async function fetchYouTubeThumbnail(identifier) {
45
- const promises = YOUTUBE_QUALITIES.map(urlFunc => fetchThumbnail(urlFunc(identifier)));
46
- const firstResult = await Promise.race(promises);
47
- return firstResult || null;
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
- module.exports = { getImageUrl };
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?: string): Promise<Track[] | null>;
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: string;
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: string): Player;
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?: string;
264
+ defaultSearchPlatform?: SearchSource;
246
265
  leaveOnEnd?: boolean;
247
- restVersion?: string;
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?: string;
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?: string;
329
+ source?: SearchSource;
292
330
  requester: any;
293
331
  nodes?: string | Node;
294
332
  }
295
333
 
296
334
  interface ResolveResponse {
297
- loadType: string;
298
- exception: any | null;
299
- playlistInfo: any | null;
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
  }