vantiv.io 1.0.7 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantiv.io",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Enterprise WebSocket infrastructure for Highrise featuring spatial intelligence systems, memory-optimized architecture, and production-grade reliability for scalable application development",
5
5
  "keywords": [
6
6
  "highrise",
@@ -23,38 +23,38 @@ class MusicClass extends EventEmitter {
23
23
 
24
24
  this.initializeStreamer();
25
25
  this.setupEventHandlers();
26
-
26
+
27
27
  if (this.config.autoStartServer) {
28
28
  this.startWebServer();
29
29
  }
30
-
30
+
31
31
  }
32
32
 
33
33
  initializeStreamer() {
34
34
  try {
35
35
  this.streamer = new IcecastStreamer(this.config.icecast);
36
- this.queue = new IcecastQueue(this.streamer, { ...this.config.queue, ...this.config.icecast});
37
-
36
+ this.queue = new IcecastQueue(this.streamer, { ...this.config.queue, ...this.config.icecast });
37
+
38
38
  this.streamer.on('playbackStart', (track) => {
39
39
  this.emit('playbackStart', track);
40
40
  });
41
-
41
+
42
42
  this.streamer.on('playbackEnd', (track) => {
43
43
  this.emit('playbackEnd', track);
44
44
  });
45
-
45
+
46
46
  this.streamer.on('progress', (data) => {
47
47
  this.emit('progress', data);
48
48
  });
49
-
49
+
50
50
  this.streamer.on('error', (error) => {
51
51
  this.emit('error', error);
52
52
  });
53
-
53
+
54
54
  this.streamer.on('streamStopped', () => {
55
55
  this.emit('streamStopped');
56
56
  });
57
-
57
+
58
58
  } catch (error) {
59
59
  console.error('Failed to initialize Icecast streamer:', error);
60
60
  throw error;
@@ -79,10 +79,19 @@ class MusicClass extends EventEmitter {
79
79
 
80
80
  let trackInfo;
81
81
  let source = 'direct';
82
-
82
+
83
83
  if (this.isValidUrl(query)) {
84
84
  if (query.includes('youtube.com') || query.includes('youtu.be')) {
85
- trackInfo = await YouTubeExtractor.getStreamUrl(query);
85
+ try {
86
+ trackInfo = await YouTubeExtractor.getStreamUrl(query);
87
+ } catch (error) {
88
+ return {
89
+ success: false,
90
+ error: error.message.includes('yt-dlp')
91
+ ? 'Missing dependency: yt-dlp'
92
+ : `YouTube error: ${error.message}`
93
+ };
94
+ }
86
95
  source = 'youtube';
87
96
  } else {
88
97
  trackInfo = {
@@ -115,7 +124,7 @@ class MusicClass extends EventEmitter {
115
124
  });
116
125
 
117
126
  const result = await this.queue.add(track);
118
-
127
+
119
128
  return {
120
129
  success: true,
121
130
  track: {
@@ -128,7 +137,7 @@ class MusicClass extends EventEmitter {
128
137
  position: result.position,
129
138
  isNowPlaying: result.isNowPlaying || false
130
139
  };
131
-
140
+
132
141
  } catch (error) {
133
142
  console.error('Error playing track:', error);
134
143
  return {
@@ -141,7 +150,7 @@ class MusicClass extends EventEmitter {
141
150
  async skip() {
142
151
  try {
143
152
  const result = await this.queue.skip();
144
-
153
+
145
154
  let upcoming = null;
146
155
  if (this.queue.queue.length > 0) {
147
156
  const nextTrack = this.queue.queue[0];
@@ -151,7 +160,7 @@ class MusicClass extends EventEmitter {
151
160
  formattedDuration: nextTrack.getFormattedDuration()
152
161
  };
153
162
  }
154
-
163
+
155
164
  return {
156
165
  success: result,
157
166
  upcoming: upcoming
@@ -197,7 +206,7 @@ class MusicClass extends EventEmitter {
197
206
  const currentIndex = modes.indexOf(this.queue.loopMode);
198
207
  const nextIndex = (currentIndex + 1) % modes.length;
199
208
  this.queue.loopMode = modes[nextIndex];
200
-
209
+
201
210
  return {
202
211
  success: true,
203
212
  newMode: this.queue.loopMode
@@ -223,6 +232,18 @@ class MusicClass extends EventEmitter {
223
232
  };
224
233
  }
225
234
 
235
+ removeFromQueue(index) {
236
+ return this.queue?.remove(index) || null;
237
+ }
238
+
239
+ moveInQueue(from, to) {
240
+ return this.queue?.move(from, to) || false;
241
+ }
242
+
243
+ clearQueue() {
244
+ return this.queue?.clear() || [];
245
+ }
246
+
226
247
  isValidUrl(string) {
227
248
  try {
228
249
  new URL(string);
@@ -234,31 +255,24 @@ class MusicClass extends EventEmitter {
234
255
 
235
256
  formatDuration(seconds) {
236
257
  if (!seconds) return '0:00';
237
-
258
+
238
259
  const hours = Math.floor(seconds / 3600);
239
260
  const minutes = Math.floor((seconds % 3600) / 60);
240
261
  const secondsRemaining = Math.floor(seconds % 60);
241
-
262
+
242
263
  if (hours > 0) {
243
264
  return `${hours}:${minutes.toString().padStart(2, '0')}:${secondsRemaining.toString().padStart(2, '0')}`;
244
265
  }
245
266
  return `${minutes}:${secondsRemaining.toString().padStart(2, '0')}`;
246
267
  }
247
268
 
248
- startWebServer() {
249
- try {
250
- this.webServer = new IcecastServer({
251
- port: this.config.serverPort,
252
- publicStreamUrl: this.streamer.publicStreamUrl,
253
- logger: this.bot.utils.logger
254
- });
255
-
256
- this.webServer.start();
257
- return true;
258
- } catch (error) {
259
- console.error('Failed to start web server:', error);
260
- return false;
261
- }
269
+ getHealth() {
270
+ return {
271
+ streamer: this.streamer?.getHealth(),
272
+ queue: this.queue?.getStatus(),
273
+ webServer: !!this.webServer,
274
+ uptime: process.uptime()
275
+ };
262
276
  }
263
277
 
264
278
  destroy() {
@@ -266,7 +280,7 @@ class MusicClass extends EventEmitter {
266
280
  if (this.streamer) {
267
281
  this.streamer.stop();
268
282
  }
269
-
283
+
270
284
  return true;
271
285
  } catch (error) {
272
286
  console.error('Error destroying music class:', error);
@@ -148,7 +148,7 @@ class IcecastStreamer extends EventEmitter {
148
148
  if (!this.config.fallbackUrl || this.isFallbackActive || this.isStreaming) {
149
149
  return false;
150
150
  }
151
-
151
+
152
152
  if (this.ffmpegProcess) {
153
153
  this.ffmpegProcess.kill('SIGKILL');
154
154
  this.ffmpegProcess = null;
@@ -156,10 +156,10 @@ class IcecastStreamer extends EventEmitter {
156
156
 
157
157
  const args = this.getFallbackFFmpegArgs();
158
158
  this.fallbackProcess = spawn('ffmpeg', args);
159
-
159
+
160
160
  this.isFallbackActive = true;
161
161
  this.isStreaming = false;
162
-
162
+
163
163
  this.currentTrack = new Track({
164
164
  url: this.config.fallbackUrl,
165
165
  title: 'Fallback Music',
@@ -173,11 +173,11 @@ class IcecastStreamer extends EventEmitter {
173
173
 
174
174
  this.fallbackProcess.stderr.on('data', (data) => {
175
175
  const message = data.toString();
176
-
176
+
177
177
  if (message.includes('Stream mapping') || message.includes('Output')) {
178
178
  this.emit('fallbackStart', this.currentTrack);
179
179
  }
180
-
180
+
181
181
  if (message.includes('Error') || message.includes('Failed')) {
182
182
  console.error('Fallback FFmpeg error:', message.trim());
183
183
  }
@@ -187,7 +187,7 @@ class IcecastStreamer extends EventEmitter {
187
187
  this.isFallbackActive = false;
188
188
  this.currentTrack = null;
189
189
  this.fallbackProcess = null;
190
-
190
+
191
191
  this.emit('fallbackEnd', { code });
192
192
  });
193
193
 
@@ -219,7 +219,7 @@ class IcecastStreamer extends EventEmitter {
219
219
  this.isStreaming = false;
220
220
  this.emit('streamStopped');
221
221
  }
222
-
222
+
223
223
  this.stopFallback();
224
224
  }
225
225
 
@@ -233,6 +233,18 @@ class IcecastStreamer extends EventEmitter {
233
233
  };
234
234
  }
235
235
 
236
+ getHealth() {
237
+ return {
238
+ status: this.isStreaming ? 'playing' :
239
+ this.isFallbackActive ? 'fallback' : 'idle',
240
+ latencyMs: this.currentTrack?.startedAt ?
241
+ Date.now() - this.currentTrack.startedAt : 0,
242
+ lastError: this.lastError,
243
+ uptime: this.currentTrack?.startedAt ?
244
+ (Date.now() - this.currentTrack.startedAt) / 1000 : 0,
245
+ };
246
+ }
247
+
236
248
  getNowPlaying() {
237
249
  if (!this.currentTrack) return null;
238
250
 
@@ -300,12 +312,12 @@ class IcecastQueue {
300
312
  this.history = [];
301
313
  this.maxHistory = config.maxHistory || 50;
302
314
  this.skipRequested = false;
303
-
315
+
304
316
  this.fallbackEnabled = config.fallbackEnabled || false;
305
317
  this.fallbackUrl = config.fallbackUrl || '';
306
318
 
307
319
  this.setupEventListeners();
308
-
320
+
309
321
  this.checkAndStartFallback();
310
322
  }
311
323
 
@@ -332,27 +344,27 @@ class IcecastQueue {
332
344
  this.streamer.on('playbackStart', (track) => {
333
345
  this.currentTrack = track;
334
346
  });
335
-
347
+
336
348
  this.streamer.on('fallbackStart', (track) => {
337
349
  this.currentTrack = track;
338
350
  });
339
-
351
+
340
352
  this.streamer.on('fallbackEnd', () => {
341
353
  this.currentTrack = null;
342
354
  this.checkAndStartFallback();
343
355
  });
344
356
  }
345
-
357
+
346
358
  checkAndStartFallback() {
347
- if (this.queue.length === 0 &&
359
+ if (this.queue.length === 0 &&
348
360
  !this.streamer.isStreaming &&
349
361
  !this.streamer.isFallbackActive &&
350
- this.fallbackEnabled &&
362
+ this.fallbackEnabled &&
351
363
  this.fallbackUrl) {
352
-
364
+
353
365
  setTimeout(() => {
354
- if (this.queue.length === 0 &&
355
- !this.streamer.isStreaming &&
366
+ if (this.queue.length === 0 &&
367
+ !this.streamer.isStreaming &&
356
368
  !this.streamer.isFallbackActive) {
357
369
  this.streamer.startFallback();
358
370
  }
@@ -453,18 +465,35 @@ class IcecastQueue {
453
465
  return this.queue.splice(position - 1, 1)[0];
454
466
  }
455
467
 
468
+ move(fromIndex, toIndex) {
469
+ if (fromIndex < 0 || fromIndex >= this.queue.length ||
470
+ toIndex < 0 || toIndex >= this.queue.length) {
471
+ return false;
472
+ }
473
+
474
+ const track = this.queue.splice(fromIndex, 1)[0];
475
+ this.queue.splice(toIndex, 0, track);
476
+ return true;
477
+ }
478
+
479
+ clearExceptCurrent() {
480
+ const cleared = this.queue.splice(0);
481
+ this.checkAndStartFallback();
482
+ return cleared;
483
+ }
484
+
456
485
  clear() {
457
486
  const cleared = [...this.queue];
458
487
  this.queue = [];
459
-
488
+
460
489
  this.checkAndStartFallback();
461
-
490
+
462
491
  return cleared;
463
492
  }
464
493
 
465
494
  addToHistory(track, success = true) {
466
495
  if (track.isFallback) return;
467
-
496
+
468
497
  const historyEntry = {
469
498
  ...track,
470
499
  playedAt: Date.now(),
@@ -499,14 +528,14 @@ class IcecastQueue {
499
528
  async delay(ms) {
500
529
  return new Promise(resolve => setTimeout(resolve, ms));
501
530
  }
502
-
531
+
503
532
  startFallback() {
504
533
  if (this.fallbackEnabled && this.fallbackUrl) {
505
534
  return this.streamer.startFallback();
506
535
  }
507
536
  return false;
508
537
  }
509
-
538
+
510
539
  stopFallback() {
511
540
  return this.streamer.stopFallback();
512
541
  }
@@ -525,12 +554,27 @@ class YouTubeExtractor {
525
554
  '--no-playlist',
526
555
  '--no-check-certificates',
527
556
  '--quiet'
528
- ]);
557
+ ], {
558
+ timeout: 15000
559
+ });
529
560
 
530
561
  let data = '';
531
562
  ytdlp.stdout.on('data', (chunk) => data += chunk.toString());
532
563
  ytdlp.stderr.on('data', () => { });
533
564
 
565
+ ytdlp.on('error', (err) => {
566
+ if (err.code === 'ENOENT') {
567
+ reject(new Error('yt-dlp not found. Install with: pip install yt-dlp'));
568
+ } else {
569
+ reject(err);
570
+ }
571
+ });
572
+
573
+ ytdlp.on('timeout', () => {
574
+ ytdlp.kill('SIGKILL');
575
+ reject(new Error('YouTube fetch timed out (15s)'));
576
+ });
577
+
534
578
  ytdlp.on('close', (code) => {
535
579
  if (code !== 0) {
536
580
  resolve(null);
@@ -34,11 +34,6 @@ class ConnectionManager extends EventEmitter {
34
34
  }
35
35
 
36
36
  _establishConnection() {
37
- if (this.reconnecting || this.connected) {
38
- this.logger.debug('ConnectionManager', 'Connection attempt already in progress — skipping');
39
- return;
40
- }
41
-
42
37
  if (!this.connectionAuth) {
43
38
  throw new Error('No connection authentication available');
44
39
  }
@@ -59,7 +54,8 @@ class ConnectionManager extends EventEmitter {
59
54
  [WebSocketConstants.HEADERS.API_TOKEN]: token,
60
55
  [WebSocketConstants.HEADERS.ROOM_ID]: roomId
61
56
  },
62
- handshakeTimeout: WebSocketConstants.DEFAULT_HANDSHAKE_TIMEOUT
57
+ handshakeTimeout: WebSocketConstants.DEFAULT_HANDSHAKE_TIMEOUT,
58
+ autoPong: true
63
59
  });
64
60
 
65
61
  this._setupWebSocketHandlers();
@@ -105,7 +101,7 @@ class ConnectionManager extends EventEmitter {
105
101
  this.reconnecting = false;
106
102
  this._clearReconnectTimeout();
107
103
 
108
- this.emit('connected');
104
+ this.emit('Connected');
109
105
  this.logger.info('ConnectionManager', 'WebSocket connection established');
110
106
  }
111
107
 
@@ -115,7 +111,7 @@ class ConnectionManager extends EventEmitter {
115
111
  this.reconnecting = false;
116
112
 
117
113
  const reasonStr = reason?.toString('utf8') || `Binary reason (length: ${reason?.length || 0})`;
118
- this.emit('disconnected', { code, reason: reasonStr });
114
+ this.emit('Disconnected', { code, reason: reasonStr });
119
115
 
120
116
  this.logger.warn('ConnectionManager', 'Connection closed', {
121
117
  code,
@@ -188,7 +184,7 @@ class ConnectionManager extends EventEmitter {
188
184
  }
189
185
 
190
186
  this.connected = false;
191
- this.emit('disconnected', { code, reason, manual: true });
187
+ this.emit('Disconnected', { code, reason, IsManual: true });
192
188
  }
193
189
 
194
190
  send(data) {
@@ -14,7 +14,7 @@ type EventType =
14
14
  | 'TipReactionEvent'
15
15
  | 'RoomModeratedEvent'
16
16
  | 'ChannelEvent'
17
-
17
+
18
18
  type Goldbars = 1 | 5 | 10 | 50 | 100 | 500 | 1000 | 5000 | 10000
19
19
  type ItemCategory =
20
20
  | "bag"
@@ -368,6 +368,12 @@ interface CooldownManagerStats {
368
368
  memoryUsage: CooldownMemoryUsage;
369
369
  }
370
370
 
371
+ interface DisconnectedData {
372
+ code: number | null,
373
+ reason: string,
374
+ IsManual: boolean
375
+ }
376
+
371
377
  interface EventMap {
372
378
  UserLeft: [user: User]
373
379
  Ready: [session: ReadyEvent]
@@ -380,6 +386,11 @@ interface EventMap {
380
386
  Direct: [user: User, message: string, conversation: Conversation]
381
387
  Moderation: [moderator: User, target: User, action: ModerationAction]
382
388
  Movement: [user: User, position: Position | null, anchor: AnchorPosition | null]
389
+
390
+ // Connections
391
+ error: [data: any]
392
+ Connected: []
393
+ Disconnected: [data: DisconnectedData]
383
394
  }
384
395
 
385
396
 
@@ -983,6 +994,468 @@ interface ChannelStats {
983
994
  listeners: number;
984
995
  }
985
996
 
997
+ /**
998
+ * Icecast stream configuration
999
+ */
1000
+ interface IcecastConfig {
1001
+ /** Icecast server hostname or IP address */
1002
+ server?: string;
1003
+ /** Icecast server port */
1004
+ port?: number;
1005
+ /** Mount point for the stream */
1006
+ mount?: string;
1007
+ /** Source password for Icecast authentication */
1008
+ sourcePassword?: string;
1009
+ /** Fallback URL to play when queue is empty */
1010
+ fallbackUrl?: string;
1011
+ /** Whether fallback is enabled */
1012
+ fallbackEnabled?: boolean;
1013
+ /** Audio format (mp3, aac, etc.) */
1014
+ audioFormat?: string;
1015
+ /** Audio bitrate (e.g., '192k') */
1016
+ audioBitrate?: string;
1017
+ /** Audio sample rate in Hz */
1018
+ audioSampleRate?: number;
1019
+ /** Number of audio channels (1 for mono, 2 for stereo) */
1020
+ audioChannels?: number;
1021
+ /** Content type for Icecast stream */
1022
+ contentType?: string;
1023
+ }
1024
+
1025
+ /**
1026
+ * Queue configuration
1027
+ */
1028
+ interface QueueConfig {
1029
+ /** Maximum number of tracks to keep in history */
1030
+ maxHistory?: number;
1031
+ }
1032
+
1033
+ /**
1034
+ * Music configuration for Highrise bot
1035
+ */
1036
+ interface MusicConfig {
1037
+ /** Whether the music module is enabled */
1038
+ enabled?: boolean;
1039
+ /** Icecast server configuration */
1040
+ icecast?: IcecastConfig;
1041
+ /** Queue management configuration */
1042
+ queue?: QueueConfig;
1043
+ /** Automatically start web server for stream monitoring */
1044
+ autoStartServer?: boolean;
1045
+ /** Port for the web server */
1046
+ serverPort?: number;
1047
+ /** Maximum queue size */
1048
+ maxQueueSize?: number;
1049
+ /** Default volume level (0-100) */
1050
+ defaultVolume?: number;
1051
+ /** Allowed audio source formats */
1052
+ allowedFormats?: string[];
1053
+ }
1054
+
1055
+ /**
1056
+ * Music playback status and information
1057
+ */
1058
+ interface MusicNowPlaying {
1059
+ /** Current track information */
1060
+ track: Track;
1061
+ /** Current playback position in seconds */
1062
+ position: number;
1063
+ /** Total track duration in seconds */
1064
+ duration: number;
1065
+ /** Playback progress percentage (0-100) */
1066
+ progress: number;
1067
+ /** Time remaining in seconds */
1068
+ remaining: number;
1069
+ /** Formatted position string (e.g., '1:30') */
1070
+ formattedPosition: string;
1071
+ /** Formatted duration string (e.g., '3:45') */
1072
+ formattedDuration: string;
1073
+ /** Formatted remaining time string (e.g., '2:15') */
1074
+ formattedRemaining: string;
1075
+ }
1076
+
1077
+ /**
1078
+ * Queue information
1079
+ */
1080
+ interface QueueInfo {
1081
+ /** Current queue of tracks */
1082
+ queue: Track[];
1083
+ /** Upcoming tracks (first 10 in queue) */
1084
+ upcoming: Track[];
1085
+ /** Current loop mode */
1086
+ loopMode: 'off' | 'track' | 'queue';
1087
+ /** Number of tracks in queue */
1088
+ length: number;
1089
+ }
1090
+
1091
+ /**
1092
+ * Play result from music operations
1093
+ */
1094
+ interface PlayResult {
1095
+ /** Whether the operation was successful */
1096
+ success: boolean;
1097
+ /** Track information (if successful) */
1098
+ track?: {
1099
+ title: string;
1100
+ duration: number;
1101
+ formattedDuration: string;
1102
+ requester: string;
1103
+ thumbnail?: string | null;
1104
+ };
1105
+ /** Position in queue (if added to queue) */
1106
+ position?: number;
1107
+ /** Whether track is now playing immediately */
1108
+ isNowPlaying?: boolean;
1109
+ /** Error message (if failed) */
1110
+ error?: string;
1111
+ }
1112
+
1113
+ /**
1114
+ * Skip result from music operations
1115
+ */
1116
+ interface SkipResult {
1117
+ /** Whether skip was successful */
1118
+ success: boolean;
1119
+ /** Upcoming track information */
1120
+ upcoming?: {
1121
+ title: string;
1122
+ duration: number;
1123
+ formattedDuration: string;
1124
+ };
1125
+ /** Error message (if failed) */
1126
+ error?: string;
1127
+ }
1128
+
1129
+ /**
1130
+ * Loop toggle result
1131
+ */
1132
+ interface LoopResult {
1133
+ /** Whether operation was successful */
1134
+ success: boolean;
1135
+ /** New loop mode */
1136
+ newMode: string;
1137
+ }
1138
+
1139
+ /**
1140
+ * Shuffle result
1141
+ */
1142
+ interface ShuffleResult {
1143
+ /** Whether shuffle was successful */
1144
+ success: boolean;
1145
+ /** Updated queue information */
1146
+ queue?: QueueInfo;
1147
+ /** Error message (if failed) */
1148
+ error?: string;
1149
+ }
1150
+
1151
+ /**
1152
+ * Music event types
1153
+ */
1154
+ interface MusicEventMap {
1155
+ 'playbackStart': [track: Track];
1156
+ 'playbackEnd': [track: Track];
1157
+ 'progress': [data: { position: number, duration: number, progress: number }];
1158
+ 'error': [error: Error];
1159
+ 'streamStopped': [];
1160
+ 'Ready': [];
1161
+ 'ready': [];
1162
+ }
1163
+
1164
+ /**
1165
+ * Logger configuration - controls how bot messages appear in console
1166
+ */
1167
+ interface LoggerOptions {
1168
+ /** Show time in logs? (true = [2023-10-05T14:30:00.68a7dd5eecae68acca580f41Z] [INFO]) */
1169
+ showTimestamp?: boolean;
1170
+ /** Show method name? (true = [connect]: Connected) */
1171
+ showMethodName?: boolean;
1172
+ /** Use colors? (true = green success, red errors) */
1173
+ colors?: boolean;
1174
+ }
1175
+
1176
+ /**
1177
+ * Bot connection settings - controls how bot handles disconnections
1178
+ */
1179
+ interface HighriseOptions {
1180
+ /** How logs look in console */
1181
+ LoggerOptions?: LoggerOptions;
1182
+ /** Wait time between reconnect tries (milliseconds) */
1183
+ reconnectDelay?: number;
1184
+ /** Auto-reconnect if disconnected? */
1185
+ autoReconnect?: boolean;
1186
+ /** Custom roles to create on startup */
1187
+ customRoles?: string[];
1188
+ /** Music module configuration */
1189
+ music?: MusicConfig;
1190
+ }
1191
+
1192
+ /**
1193
+ * Configuration options for request senders
1194
+ */
1195
+ interface SenderConfig {
1196
+ /**
1197
+ * Default timeout for requests waiting in milliseconds (default: 10000ms)
1198
+ */
1199
+ defaultTimeout?: number;
1200
+
1201
+ /**
1202
+ * Maximum number of retry attempts for failed requests (default: 2)
1203
+ */
1204
+ maxRetries?: number;
1205
+
1206
+ /**
1207
+ * Delay between retry attempts in milliseconds (default: 100)
1208
+ */
1209
+ retryDelay?: number;
1210
+ }
1211
+
1212
+ interface PlayMetadata {
1213
+ title?: string;
1214
+ duration?: number;
1215
+ requester?: string;
1216
+ requesterId?: string;
1217
+ thumbnail?: string | null;
1218
+ source?: string;
1219
+ }
1220
+
1221
+ interface MusicHealthInfo {
1222
+ streamer: {
1223
+ status: 'idle' | 'playing' | 'fallback' | 'error';
1224
+ latencyMs: number;
1225
+ uptimeSeconds: number;
1226
+ lastError?: string;
1227
+ };
1228
+ queue: {
1229
+ length: number;
1230
+ isProcessing: boolean;
1231
+ };
1232
+ webServer: boolean;
1233
+ systemUptime: number;
1234
+ }
1235
+
1236
+ /**
1237
+ * Track information
1238
+ */
1239
+ class Track {
1240
+ id: number;
1241
+ url: string;
1242
+ title: string;
1243
+ duration: number;
1244
+ requester: string;
1245
+ requesterId: string;
1246
+ addedAt: number;
1247
+ thumbnail?: string | null;
1248
+ source: string;
1249
+ isFallback?: boolean;
1250
+ position?: number;
1251
+ startedAt?: number;
1252
+
1253
+ /**
1254
+ * Formats track duration (in seconds) as a readable time string.
1255
+ *
1256
+ * @returns
1257
+ * - `"Unknown"` if duration is `0`, `null`, or `invalid`
1258
+ * - `"M:SS"` for tracks under 1 hour (e.g., `"0:05"`, `"3:45"`)
1259
+ * - `"H:MM:SS"` for tracks 1+ hours (e.g., `"1:05:30"`)
1260
+ *
1261
+ * Note: Minutes are not padded in M:SS mode (`0:05`, not `00:05`),
1262
+ * but minutes/seconds are padded in H:MM:SS mode.
1263
+ */
1264
+ getFormattedDuration(): string;
1265
+ }
1266
+
1267
+ declare class MusicClass {
1268
+ /**
1269
+ * Creates a new MusicClass instance
1270
+ * @param bot - Highrise bot instance
1271
+ * @param config - Music configuration
1272
+ */
1273
+ constructor(bot: Highrise, config: MusicConfig);
1274
+
1275
+ /**
1276
+ * Play a track from URL or search query
1277
+ * @param query - YouTube URL, direct URL, or search query
1278
+ * @param metadata - Track metadata (requester, title, etc.)
1279
+ * @returns Promise resolving to play result or error when thrown
1280
+ * @example
1281
+ * ```typescript
1282
+ * // Play YouTube video by URL
1283
+ * const result = await bot.music.play(
1284
+ * 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
1285
+ * { requester: 'User123', requesterId: 'user_123' }
1286
+ * );
1287
+ *
1288
+ * // Search and play YouTube video
1289
+ * const result = await bot.music.play(
1290
+ * 'never gonna give you up',
1291
+ * { requester: 'User123', requesterId: 'user_123' }
1292
+ * );
1293
+ * ```
1294
+ */
1295
+ play(query: string, metadata?: PlayMetadata): Promise<PlayResult>;
1296
+
1297
+ /**
1298
+ * Skip the currently playing track
1299
+ * @returns Promise resolving to skip result
1300
+ * @example
1301
+ * ```typescript
1302
+ * const result = await bot.music.skip();
1303
+ * if (result.success && result.upcoming) {
1304
+ * await bot.message.send(`Now playing: ${result.upcoming.title}`);
1305
+ * }
1306
+ * ```
1307
+ */
1308
+ skip(): Promise<SkipResult>;
1309
+
1310
+ /**
1311
+ * Get information about currently playing track
1312
+ * @returns Now playing information or null if nothing is playing
1313
+ * @example
1314
+ * ```typescript
1315
+ * const np = bot.music.getNowPlaying();
1316
+ * if (np) {
1317
+ * await bot.message.send(
1318
+ * `Now playing: ${np.track.title}\n` +
1319
+ * `Progress: ${np.formattedPosition} / ${np.formattedDuration}`
1320
+ * );
1321
+ * }
1322
+ * ```
1323
+ */
1324
+ getNowPlaying(): MusicNowPlaying | null;
1325
+
1326
+ /**
1327
+ * Get current queue information
1328
+ * @returns Queue information
1329
+ * @example
1330
+ * ```typescript
1331
+ * const queue = bot.music.getQueue();
1332
+ * await bot.message.send(
1333
+ * `Queue: ${queue.length} tracks\n` +
1334
+ * `Loop mode: ${queue.loopMode}`
1335
+ * );
1336
+ * ```
1337
+ */
1338
+ getQueue(): QueueInfo;
1339
+
1340
+ /**
1341
+ * Toggle loop mode between off, track, and queue
1342
+ * @returns Loop result with new mode
1343
+ * @example
1344
+ * ```typescript
1345
+ * const result = bot.music.toggleLoop();
1346
+ * await bot.message.send(`Loop mode: ${result.newMode}`);
1347
+ * ```
1348
+ */
1349
+ toggleLoop(): LoopResult;
1350
+
1351
+ /**
1352
+ * Shuffle the current queue
1353
+ * @returns Shuffle result
1354
+ * @example
1355
+ * ```typescript
1356
+ * const result = bot.music.shuffle();
1357
+ * if (result.success) {
1358
+ * await bot.message.send('Queue shuffled!');
1359
+ * }
1360
+ * ```
1361
+ */
1362
+ shuffle(): ShuffleResult;
1363
+
1364
+ /**
1365
+ * Remove a track from queue by position (1-based index)
1366
+ * @param position - Position in queue (1 = next track)
1367
+ * @returns Removed track or null if position invalid
1368
+ * @example
1369
+ * ```typescript
1370
+ * const removed = bot.music.removeFromQueue(1);
1371
+ * if (removed) {
1372
+ * await bot.message.send(`Removed: ${removed.title}`);
1373
+ * }
1374
+ * ```
1375
+ */
1376
+ removeFromQueue(position: number): Track | null;
1377
+
1378
+ /**
1379
+ * Move a track from one position to another in the queue
1380
+ * @tutorial 1 being the top
1381
+ * @param fromPosition - Current position (1-based index)
1382
+ * @param toPosition - Target position (1-based index)
1383
+ * @returns true if move succeeded, false if invalid positions
1384
+ * @example
1385
+ * ```typescript
1386
+ * const success = bot.music.moveInQueue(3, 1);
1387
+ * if (success) {
1388
+ * await bot.message.send('Moved track to top of queue!');
1389
+ * }
1390
+ * ```
1391
+ */
1392
+ moveInQueue(fromPosition: number, toPosition: number): boolean;
1393
+
1394
+ /**
1395
+ * Clear the entire queue (keeps currently playing track)
1396
+ * @returns Number of tracks removed
1397
+ * @example
1398
+ * ```typescript
1399
+ * const count = bot.music.clearQueue();
1400
+ * await bot.message.send(`Cleared ${count} tracks from queue`);
1401
+ * ```
1402
+ */
1403
+ clearQueue(): number;
1404
+
1405
+ /**
1406
+ * Get system health status
1407
+ * @returns Health information including stream status, latency, and errors
1408
+ * @example
1409
+ * ```typescript
1410
+ * const health = bot.music.getHealth();
1411
+ * if (health.streamer.status === 'error') {
1412
+ * await bot.message.send(`⚠️ Music system error: ${health.streamer.lastError}`);
1413
+ * }
1414
+ * ```
1415
+ */
1416
+ getHealth(): MusicHealthInfo;
1417
+
1418
+ /**
1419
+ * Check if string is a valid URL
1420
+ * @param string - String to check
1421
+ * @returns Whether string is a valid URL
1422
+ */
1423
+ isValidUrl(string: string): boolean;
1424
+
1425
+ /**
1426
+ * Format duration in seconds to human-readable string
1427
+ * @param seconds - Duration in seconds
1428
+ * @returns Formatted duration string in `M:SS` format (e.g. `"0:00"`, `"1:05"`, `"12:30"`)
1429
+ */
1430
+ formatDuration(seconds: number): string;
1431
+
1432
+ /**
1433
+ * Clean up and destroy music instance
1434
+ * @returns Whether cleanup was successful
1435
+ */
1436
+ destroy(): boolean;
1437
+
1438
+ /**
1439
+ * Register event listener for music events
1440
+ * @param event - Event name
1441
+ * @param listener - Event handler function
1442
+ */
1443
+ on<K extends keyof MusicEventMap>(
1444
+ event: K,
1445
+ listener: (...args: MusicEventMap[K]) => void
1446
+ ): void;
1447
+
1448
+ /**
1449
+ * Remove event listener
1450
+ * @param event - Event name
1451
+ * @param listener - Event handler function to remove
1452
+ */
1453
+ off<K extends keyof MusicEventMap>(
1454
+ event: K,
1455
+ listener: (...args: MusicEventMap[K]) => void
1456
+ ): void;
1457
+ }
1458
+
986
1459
  declare class RoleManager {
987
1460
  /**
988
1461
  * Create a new custom role
@@ -1716,10 +2189,6 @@ declare class CooldownManager {
1716
2189
  static formatTime(ms: number): string;
1717
2190
  }
1718
2191
 
1719
- /**
1720
- * Hidden channel system for bot-to-bot communication
1721
- * Messages are broadcast to all bots in the same room
1722
- */
1723
2192
  declare class Channel {
1724
2193
  /**
1725
2194
  * Send a message to the hidden channel
@@ -2613,7 +3082,6 @@ declare class WebApi {
2613
3082
  }
2614
3083
  }
2615
3084
 
2616
-
2617
3085
  declare class MovementCache {
2618
3086
  /**
2619
3087
  * Get user data by either userId or username
@@ -2974,7 +3442,6 @@ declare class WhisperClass {
2974
3442
  send(user_id: string, message: string): Promise<boolean>;
2975
3443
  }
2976
3444
 
2977
-
2978
3445
  declare class DirectClass {
2979
3446
 
2980
3447
  /**
@@ -3345,7 +3812,7 @@ declare class Logger {
3345
3812
  debug(method: string, message: string, data?: any): void;
3346
3813
  }
3347
3814
 
3348
- interface BotUtils {
3815
+ declare class BotUtils {
3349
3816
  /**
3350
3817
  * Logger instance for structured, color-coded logging throughout the application
3351
3818
  * Provides different log levels (SUCCESS, ERROR, WARN, INFO, DEBUG) with timestamps and method tracking
@@ -3612,52 +4079,6 @@ declare class RoomClass {
3612
4079
  }
3613
4080
  }
3614
4081
 
3615
- /**
3616
- * Logger configuration - controls how bot messages appear in console
3617
- */
3618
- interface LoggerOptions {
3619
- /** Show time in logs? (true = [2023-10-05T14:30:00.68a7dd5eecae68acca580f41Z] [INFO]) */
3620
- showTimestamp?: boolean;
3621
- /** Show method name? (true = [connect]: Connected) */
3622
- showMethodName?: boolean;
3623
- /** Use colors? (true = green success, red errors) */
3624
- colors?: boolean;
3625
- }
3626
-
3627
- /**
3628
- * Bot connection settings - controls how bot handles disconnections
3629
- */
3630
- interface HighriseOptions {
3631
- /** How logs look in console */
3632
- LoggerOptions?: LoggerOptions;
3633
- /** Wait time between reconnect tries (milliseconds) */
3634
- reconnectDelay: number;
3635
- /** Auto-reconnect if disconnected? */
3636
- autoReconnect: boolean;
3637
- /** Custom roles to create on startup */
3638
- customRoles?: string[];
3639
- }
3640
-
3641
- /**
3642
- * Configuration options for request senders
3643
- */
3644
- interface SenderConfig {
3645
- /**
3646
- * Default timeout for requests waiting in milliseconds (default: 10000ms)
3647
- */
3648
- defaultTimeout?: number;
3649
-
3650
- /**
3651
- * Maximum number of retry attempts for failed requests (default: 2)
3652
- */
3653
- maxRetries?: number;
3654
-
3655
- /**
3656
- * Delay between retry attempts in milliseconds (default: 100)
3657
- */
3658
- retryDelay?: number;
3659
- }
3660
-
3661
4082
  declare class Highrise {
3662
4083
  /**
3663
4084
  * Creates a new Highrise bot instance
@@ -3713,6 +4134,9 @@ declare class Highrise {
3713
4134
  */
3714
4135
  public channel: Channel;
3715
4136
 
4137
+ /** Music streaming and management */
4138
+ public music: MusicClass | null;
4139
+
3716
4140
  /**
3717
4141
  * Cache management system for efficient data storage and retrieval
3718
4142
  * Provides optimized memory usage and fast lookups for frequently accessed data