highrise.bot 1.0.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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/index.js +13 -0
  4. package/package.json +29 -0
  5. package/src/classes/Actions/Awaiter.js +203 -0
  6. package/src/classes/Actions/Channel.js +327 -0
  7. package/src/classes/Actions/Direct.js +390 -0
  8. package/src/classes/Actions/Inventory.js +193 -0
  9. package/src/classes/Actions/Music.js +243 -0
  10. package/src/classes/Actions/Outfit.js +175 -0
  11. package/src/classes/Actions/Player.js +604 -0
  12. package/src/classes/Actions/Public.js +143 -0
  13. package/src/classes/Actions/Room.js +495 -0
  14. package/src/classes/Actions/Utils.js +77 -0
  15. package/src/classes/Actions/lib/AudioStreaming.js +695 -0
  16. package/src/classes/Caches/MovementCache.js +364 -0
  17. package/src/classes/Handlers/AxiosErrorHandler.js +68 -0
  18. package/src/classes/Handlers/ErrorHandler.js +65 -0
  19. package/src/classes/Handlers/EventHandlers.js +193 -0
  20. package/src/classes/Handlers/WebSocketHandlers.js +126 -0
  21. package/src/classes/Managers/CooldownManager.js +516 -0
  22. package/src/classes/Managers/DanceFloorManagers.js +609 -0
  23. package/src/classes/Managers/Helpers/CleanupManager.js +130 -0
  24. package/src/classes/Managers/Helpers/HighriseError.js +107 -0
  25. package/src/classes/Managers/Helpers/HighriseResponse.js +33 -0
  26. package/src/classes/Managers/Helpers/LoggerManager.js +171 -0
  27. package/src/classes/Managers/Helpers/MetricsManager.js +83 -0
  28. package/src/classes/Managers/Networking/ConnectionManager.js +253 -0
  29. package/src/classes/Managers/Networking/EventsManager.js +64 -0
  30. package/src/classes/Managers/Networking/KeepAliveManager.js +58 -0
  31. package/src/classes/Managers/Networking/MessageHandler.js +123 -0
  32. package/src/classes/Managers/Networking/Request.js +323 -0
  33. package/src/classes/Managers/RoleManager.js +322 -0
  34. package/src/classes/WebApi/Category/Grab.js +98 -0
  35. package/src/classes/WebApi/Category/Item.js +347 -0
  36. package/src/classes/WebApi/Category/Post.js +154 -0
  37. package/src/classes/WebApi/Category/Room.js +137 -0
  38. package/src/classes/WebApi/Category/User.js +88 -0
  39. package/src/classes/WebApi/webapi.js +52 -0
  40. package/src/constants/ErrorConstants.js +109 -0
  41. package/src/constants/TypesConstants.js +91 -0
  42. package/src/constants/WebSocketConstants.js +78 -0
  43. package/src/core/Highrise.js +192 -0
  44. package/src/core/HighriseWebsocket.js +242 -0
  45. package/src/utils/ConvertSvgToPng.js +51 -0
  46. package/src/utils/Job.js +130 -0
  47. package/src/utils/ModelPool.js +160 -0
  48. package/src/utils/Models.js +128 -0
  49. package/src/utils/versionCheck.js +27 -0
  50. package/src/validators/ConfigValidator.js +195 -0
  51. package/src/validators/ConnectionValidator.js +65 -0
  52. package/typings/index.d.ts +5042 -0
@@ -0,0 +1,695 @@
1
+ const express = require('express');
2
+ const { spawn } = require('child_process');
3
+ const EventEmitter = require('events');
4
+ const fs = require('fs').promises;
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ class IcecastStreamer extends EventEmitter {
9
+ constructor(config = {}) {
10
+ super();
11
+ this.config = {
12
+ server: config.server || 'localhost',
13
+ port: config.port || 8000,
14
+ mount: config.mount || '/stream',
15
+ sourcePassword: config.sourcePassword || 'hackme',
16
+ fallbackUrl: config.fallbackUrl || '',
17
+ fallbackEnabled: config.fallbackEnabled || false,
18
+ audioFormat: config.audioFormat || 'mp3',
19
+ audioBitrate: config.audioBitrate || '128k',
20
+ audioSampleRate: config.audioSampleRate || 48000,
21
+ audioChannels: config.audioChannels || 2,
22
+ contentType: config.contentType || 'audio/mpeg'
23
+ };
24
+
25
+ this.ffmpegProcess = null;
26
+ this.fallbackProcess = null;
27
+ this.isStreaming = false;
28
+ this.isFallbackActive = false;
29
+ this.songEmitted = false;
30
+ this.fallbackEmitted = false;
31
+ this.currentTrack = null;
32
+ this.tempFilesToDelete = new Set();
33
+
34
+ this.icecastUrl = `icecast://source:${this.config.sourcePassword}@${this.config.server}:${this.config.port}${this.config.mount}`;
35
+ this.publicStreamUrl = `${this.config.server}:${this.config.port}${this.config.mount}`;
36
+ }
37
+
38
+ getFFmpegArgs(url) {
39
+ const args = [];
40
+
41
+ args.push(
42
+ '-re',
43
+ '-i', url,
44
+ '-f', this.config.audioFormat,
45
+ '-content_type', this.config.contentType,
46
+ '-vn',
47
+ '-ar', this.config.audioSampleRate.toString(),
48
+ '-ac', this.config.audioChannels.toString(),
49
+ '-b:a', this.config.audioBitrate,
50
+ this.icecastUrl
51
+ );
52
+
53
+ return args;
54
+ }
55
+
56
+ getFallbackFFmpegArgs() {
57
+ return [
58
+ '-re',
59
+ '-reconnect', '1',
60
+ '-reconnect_at_eof', '1',
61
+ '-reconnect_streamed', '1',
62
+ '-reconnect_delay_max', '5',
63
+ '-i', this.config.fallbackUrl,
64
+ '-f', this.config.audioFormat,
65
+ '-content_type', this.config.contentType,
66
+ '-vn',
67
+ '-ar', this.config.audioSampleRate.toString(),
68
+ '-ac', this.config.audioChannels.toString(),
69
+ '-b:a', this.config.audioBitrate,
70
+ this.icecastUrl
71
+ ];
72
+ }
73
+
74
+ async streamToIcecast(url, metadata = {}) {
75
+ if (this.isFallbackActive) {
76
+ this.stopFallback();
77
+ await this.delay(200);
78
+ }
79
+
80
+ if (this.ffmpegProcess) {
81
+ this.stop();
82
+ await this.delay(200);
83
+ }
84
+
85
+ return new Promise((resolve, reject) => {
86
+ this.currentTrack = new Track({
87
+ url,
88
+ title: metadata.title || 'Unknown Track',
89
+ duration: metadata.duration || 0,
90
+ requester: metadata.requester || 'System',
91
+ requesterId: metadata.requesterId || 'system',
92
+ thumbnail: metadata.thumbnail,
93
+ source: metadata.source || 'stream',
94
+ startedAt: Date.now(),
95
+ isTempFile: metadata.isTempFile || false
96
+ });
97
+
98
+ if (metadata.isTempFile) {
99
+ this.tempFilesToDelete.add(url);
100
+ }
101
+
102
+ this.isStreaming = true;
103
+ this.isFallbackActive = false;
104
+
105
+ const args = this.getFFmpegArgs(url);
106
+ this.ffmpegProcess = spawn('ffmpeg', args);
107
+
108
+ this.ffmpegProcess.stderr.on('data', (data) => {
109
+ const message = data.toString();
110
+
111
+ const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
112
+ if (durationMatch) {
113
+ const hours = parseInt(durationMatch[1]);
114
+ const minutes = parseInt(durationMatch[2]);
115
+ const seconds = parseFloat(durationMatch[3]);
116
+ this.currentTrack.duration = hours * 3600 + minutes * 60 + seconds;
117
+ }
118
+
119
+ const timeMatch = message.match(/time=(\d+):(\d+):(\d+\.\d+)/);
120
+ if (timeMatch) {
121
+ const hours = parseInt(timeMatch[1]);
122
+ const minutes = parseInt(timeMatch[2]);
123
+ const seconds = parseFloat(timeMatch[3]);
124
+ this.currentTrack.position = hours * 3600 + minutes * 60 + seconds;
125
+
126
+ this.emit('progress', {
127
+ position: this.currentTrack.position,
128
+ duration: this.currentTrack.duration,
129
+ percentage: this.currentTrack.duration > 0 ?
130
+ (this.currentTrack.position / this.currentTrack.duration) * 100 : 0
131
+ });
132
+ }
133
+
134
+ if (!this.songEmitted && (message.includes('Stream mapping') || message.includes('Output'))) {
135
+ this.songEmitted = true;
136
+ this.emit('playbackStart', this.currentTrack);
137
+ resolve();
138
+ }
139
+
140
+ if (message.includes('Error') || message.includes('Failed')) {
141
+ console.error('FFmpeg error:', message.trim());
142
+ this.emit('error', new Error(message.trim()));
143
+ }
144
+ });
145
+
146
+ this.ffmpegProcess.on('close', async (code) => {
147
+ const endedTrack = this.currentTrack;
148
+
149
+ this.isStreaming = false;
150
+ this.currentTrack = null;
151
+ this.ffmpegProcess = null;
152
+ this.songEmitted = false;
153
+
154
+ if (endedTrack?.url && this.tempFilesToDelete.has(endedTrack.url)) {
155
+ try {
156
+ await fs.unlink(endedTrack.url);
157
+ this.tempFilesToDelete.delete(endedTrack.url);
158
+ } catch { }
159
+ }
160
+
161
+ this.emit('playbackEnd', endedTrack);
162
+ this.emit('streamEnd', { code, track: endedTrack });
163
+ });
164
+
165
+ this.ffmpegProcess.on('error', async (err) => {
166
+ console.error('FFmpeg process error:', err);
167
+ this.isStreaming = false;
168
+ this.currentTrack = null;
169
+ this.songEmitted = false;
170
+
171
+ if (this.currentTrack?.url && this.tempFilesToDelete.has(this.currentTrack.url)) {
172
+ try {
173
+ await fs.unlink(this.currentTrack.url);
174
+ this.tempFilesToDelete.delete(this.currentTrack.url);
175
+ } catch { }
176
+ }
177
+
178
+ reject(err);
179
+ this.emit('error', err);
180
+ });
181
+ });
182
+ }
183
+
184
+ startFallback() {
185
+ if (!this.config.fallbackUrl || this.isFallbackActive || this.isStreaming) {
186
+ return false;
187
+ }
188
+
189
+ if (this.ffmpegProcess) {
190
+ this.ffmpegProcess.kill('SIGKILL');
191
+ this.ffmpegProcess = null;
192
+ }
193
+
194
+ const args = this.getFallbackFFmpegArgs();
195
+ this.fallbackProcess = spawn('ffmpeg', args);
196
+
197
+ this.isFallbackActive = true;
198
+ this.isStreaming = false;
199
+
200
+ this.currentTrack = new Track({
201
+ url: this.config.fallbackUrl,
202
+ title: 'Fallback Music',
203
+ duration: 0,
204
+ requester: 'System',
205
+ requesterId: 'system',
206
+ source: 'fallback',
207
+ startedAt: Date.now(),
208
+ isFallback: true
209
+ });
210
+
211
+ this.fallbackProcess.stderr.on('data', (data) => {
212
+ const message = data.toString();
213
+
214
+ if (!this.fallbackEmitted && (message.includes('Stream mapping') || message.includes('Output'))) {
215
+ this.fallbackEmitted = true;
216
+ this.emit('fallbackStart', this.currentTrack);
217
+ }
218
+
219
+ if (message.includes('Error') || message.includes('Failed')) {
220
+ console.error('Fallback FFmpeg error:', message.trim());
221
+ }
222
+ });
223
+
224
+ this.fallbackProcess.on('close', (code) => {
225
+ this.isFallbackActive = false;
226
+ this.currentTrack = null;
227
+ this.fallbackProcess = null;
228
+ this.fallbackEmitted = false;
229
+ this.emit('fallbackEnd', { code });
230
+ });
231
+
232
+ this.fallbackProcess.on('error', (err) => {
233
+ console.error('Fallback FFmpeg process error:', err);
234
+ this.isFallbackActive = false;
235
+ this.fallbackEmitted = false;
236
+ this.currentTrack = null;
237
+ });
238
+
239
+ return true;
240
+ }
241
+
242
+ stopFallback() {
243
+ if (this.fallbackProcess) {
244
+ this.fallbackProcess.kill('SIGKILL');
245
+ this.fallbackProcess = null;
246
+ this.isFallbackActive = false;
247
+ this.currentTrack = null;
248
+ this.fallbackEmitted = false;
249
+ this.emit('fallbackStopped');
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+
255
+ stop() {
256
+ if (this.ffmpegProcess) {
257
+ this.ffmpegProcess.kill('SIGKILL');
258
+ this.ffmpegProcess = null;
259
+ this.isStreaming = false;
260
+ this.songEmitted = false;
261
+ this.emit('streamStopped');
262
+ }
263
+
264
+ this.stopFallback();
265
+ }
266
+
267
+ getStatus() {
268
+ return {
269
+ isStreaming: this.isStreaming,
270
+ isFallbackActive: this.isFallbackActive,
271
+ currentTrack: this.currentTrack,
272
+ config: this.config,
273
+ publicStreamUrl: this.publicStreamUrl
274
+ };
275
+ }
276
+
277
+ getHealth() {
278
+ return {
279
+ status: this.isStreaming ? 'playing' :
280
+ this.isFallbackActive ? 'fallback' : 'idle',
281
+ latencyMs: this.currentTrack?.startedAt ?
282
+ Date.now() - this.currentTrack.startedAt : 0,
283
+ lastError: this.lastError,
284
+ uptime: this.currentTrack?.startedAt ?
285
+ (Date.now() - this.currentTrack.startedAt) / 1000 : 0,
286
+ };
287
+ }
288
+
289
+ getNowPlaying() {
290
+ if (!this.currentTrack) return null;
291
+
292
+ return {
293
+ track: this.currentTrack,
294
+ position: this.currentTrack.position || 0,
295
+ duration: this.currentTrack.duration || 0,
296
+ progress: this.currentTrack.duration > 0 ?
297
+ ((this.currentTrack.position || 0) / this.currentTrack.duration) * 100 : 0,
298
+ remaining: this.currentTrack.duration > 0 ?
299
+ this.currentTrack.duration - (this.currentTrack.position || 0) : 0,
300
+ elapsed: this.currentTrack.startedAt ?
301
+ Math.floor((Date.now() - this.currentTrack.startedAt) / 1000) : 0
302
+ };
303
+ }
304
+
305
+ updateConfig(newConfig) {
306
+ this.config = { ...this.config, ...newConfig };
307
+ this.icecastUrl = `icecast://source:${this.config.sourcePassword}@${this.config.server}:${this.config.port}${this.config.mount}`;
308
+ this.publicStreamUrl = `http://${this.config.server}:${this.config.port}${this.config.mount}`;
309
+ console.log('Icecast config updated');
310
+ return this.config;
311
+ }
312
+
313
+ async delay(ms) {
314
+ return new Promise(resolve => setTimeout(resolve, ms));
315
+ }
316
+ }
317
+
318
+ class Track {
319
+ constructor(data) {
320
+ this.id = data.id || Date.now();
321
+ this.url = data.url;
322
+ this.title = data.title.replace('@', '');
323
+ this.duration = data.duration;
324
+ this.requester = data.requester;
325
+ this.requesterId = data.requesterId;
326
+ this.addedAt = Date.now();
327
+ this.thumbnail = data.thumbnail;
328
+ this.source = data.source;
329
+ this.isFallback = data.isFallback || false;
330
+ this.isTempFile = data.isTempFile || false;
331
+ }
332
+
333
+ getFormattedDuration() {
334
+ if (!this.duration) return 'Unknown';
335
+ const hours = Math.floor(this.duration / 3600);
336
+ const minutes = Math.floor((this.duration % 3600) / 60);
337
+ const seconds = Math.floor(this.duration % 60);
338
+ if (hours > 0) {
339
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
340
+ }
341
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
342
+ }
343
+ }
344
+
345
+ class IcecastQueue {
346
+ constructor(streamer, config = {}) {
347
+ this.queue = [];
348
+ this.streamer = streamer;
349
+ this.isProcessing = false;
350
+ this.currentTrack = null;
351
+ this.loopMode = 'off';
352
+ this.history = [];
353
+ this.maxHistory = config.maxHistory || 50;
354
+ this.skipRequested = false;
355
+ this.fallbackEnabled = config.fallbackEnabled || false;
356
+ this.fallbackUrl = config.fallbackUrl || '';
357
+ this.setupEventListeners();
358
+ this.checkAndStartFallback();
359
+ }
360
+
361
+ setupEventListeners() {
362
+ this.streamer.on('playbackEnd', (track) => {
363
+ if (track && !track.isFallback) {
364
+ this.addToHistory(track, true);
365
+ }
366
+ if (this.skipRequested) {
367
+ this.skipRequested = false;
368
+ return;
369
+ }
370
+ if (this.loopMode === 'track' && track && !track.isFallback) {
371
+ this.queue.unshift(new Track({ ...track }));
372
+ } else if (this.loopMode === 'queue' && track && !track.isFallback) {
373
+ this.queue.push(new Track({ ...track }));
374
+ }
375
+ setTimeout(() => this.playNext(), 300);
376
+ });
377
+
378
+ this.streamer.on('playbackStart', (track) => {
379
+ this.currentTrack = track;
380
+ });
381
+
382
+ this.streamer.on('fallbackStart', (track) => {
383
+ this.currentTrack = track;
384
+ });
385
+
386
+ this.streamer.on('fallbackEnd', () => {
387
+ this.currentTrack = null;
388
+ this.checkAndStartFallback();
389
+ });
390
+ }
391
+
392
+ checkAndStartFallback() {
393
+ if (this.queue.length === 0 &&
394
+ !this.streamer.isStreaming &&
395
+ !this.streamer.isFallbackActive &&
396
+ this.fallbackEnabled &&
397
+ this.fallbackUrl) {
398
+ setTimeout(() => {
399
+ if (this.queue.length === 0 &&
400
+ !this.streamer.isStreaming &&
401
+ !this.streamer.isFallbackActive) {
402
+ this.streamer.startFallback();
403
+ }
404
+ }, 1000);
405
+ }
406
+ }
407
+
408
+ async add(track) {
409
+ const isNewTrack = !this.queue.some(q => q.id === track.id);
410
+ if (!isNewTrack) {
411
+ return { position: -1, added: false, reason: 'duplicate' };
412
+ }
413
+ this.queue.push(track);
414
+ const position = this.queue.length;
415
+ if (!this.streamer.isStreaming && this.queue.length === 1) {
416
+ await this.playNext();
417
+ return { position: 1, added: true, isNowPlaying: true };
418
+ }
419
+ return { position, added: true, isNowPlaying: false };
420
+ }
421
+
422
+ async addImmediate(track) {
423
+ if (this.currentTrack && !this.currentTrack.isFallback) {
424
+ this.queue.unshift(new Track({ ...this.currentTrack }));
425
+ }
426
+ this.queue.unshift(track);
427
+ await this.skip();
428
+ return { position: 1, added: true, isNowPlaying: true };
429
+ }
430
+
431
+ async addFromYouTube(input, metadata) {
432
+ let trackData;
433
+ if (input.startsWith('http')) {
434
+ trackData = await YouTubeExtractor.downloadAudioToTempFile(input);
435
+ } else {
436
+ trackData = await YouTubeExtractor.searchAndDownload(input);
437
+ }
438
+ trackData.requester = metadata.requester
439
+ trackData.requesterId = metadata.requesterId
440
+
441
+ const track = new Track(trackData);
442
+ return await this.add(track);
443
+ }
444
+
445
+ async playNext() {
446
+ if (this.isProcessing) return false;
447
+ this.isProcessing = true;
448
+ try {
449
+ if (this.streamer.isFallbackActive) {
450
+ this.streamer.stopFallback();
451
+ await this.delay(200);
452
+ }
453
+ if (this.queue.length === 0) {
454
+ this.checkAndStartFallback();
455
+ return false;
456
+ }
457
+ const nextTrack = this.queue.shift();
458
+ if (!nextTrack?.url) {
459
+ setTimeout(() => this.playNext(), 1000);
460
+ return false;
461
+ }
462
+ await this.streamer.streamToIcecast(nextTrack.url, {
463
+ title: nextTrack.title,
464
+ duration: nextTrack.duration,
465
+ requester: nextTrack.requester,
466
+ requesterId: nextTrack.requesterId,
467
+ thumbnail: nextTrack.thumbnail,
468
+ source: nextTrack.source,
469
+ isTempFile: nextTrack.isTempFile
470
+ });
471
+ return true;
472
+ } catch (error) {
473
+ console.error('Error playing next track:', error);
474
+ setTimeout(() => this.playNext(), 1000);
475
+ return false;
476
+ } finally {
477
+ this.isProcessing = false;
478
+ }
479
+ }
480
+
481
+ async skip() {
482
+ if (!this.streamer.isStreaming && this.streamer.isFallbackActive) {
483
+ return false;
484
+ }
485
+ this.skipRequested = true;
486
+ this.streamer.stop();
487
+ await this.delay(100);
488
+ await this.playNext();
489
+ return true;
490
+ }
491
+
492
+ remove(position) {
493
+ if (position < 1 || position > this.queue.length) return null;
494
+ return this.queue.splice(position - 1, 1)[0];
495
+ }
496
+
497
+ move(fromIndex, toIndex) {
498
+ if (fromIndex < 0 || fromIndex >= this.queue.length ||
499
+ toIndex < 0 || toIndex >= this.queue.length) {
500
+ return false;
501
+ }
502
+ const track = this.queue.splice(fromIndex, 1)[0];
503
+ this.queue.splice(toIndex, 0, track);
504
+ return true;
505
+ }
506
+
507
+ clearExceptCurrent() {
508
+ const cleared = this.queue.splice(0);
509
+ this.checkAndStartFallback();
510
+ return cleared;
511
+ }
512
+
513
+ clear() {
514
+ const cleared = [...this.queue];
515
+ this.queue = [];
516
+ this.checkAndStartFallback();
517
+ return cleared;
518
+ }
519
+
520
+ addToHistory(track, success = true) {
521
+ if (track.isFallback) return;
522
+ const historyEntry = {
523
+ ...track,
524
+ playedAt: Date.now(),
525
+ success,
526
+ endedAt: Date.now()
527
+ };
528
+ this.history.unshift(historyEntry);
529
+ if (this.history.length > this.maxHistory) {
530
+ this.history.pop();
531
+ }
532
+ }
533
+
534
+ getQueue() {
535
+ return [...this.queue];
536
+ }
537
+
538
+ getStatus() {
539
+ return {
540
+ isPlaying: this.streamer.isStreaming || this.streamer.isFallbackActive,
541
+ isMusicPlaying: this.streamer.isStreaming,
542
+ isFallbackActive: this.streamer.isFallbackActive,
543
+ currentTrack: this.currentTrack,
544
+ queueLength: this.queue.length,
545
+ loopMode: this.loopMode,
546
+ historyLength: this.history.length,
547
+ upcoming: this.queue.slice(0, 10)
548
+ };
549
+ }
550
+
551
+ async delay(ms) {
552
+ return new Promise(resolve => setTimeout(resolve, ms));
553
+ }
554
+
555
+ startFallback() {
556
+ if (this.fallbackEnabled && this.fallbackUrl) {
557
+ return this.streamer.startFallback();
558
+ }
559
+ return false;
560
+ }
561
+
562
+ stopFallback() {
563
+ return this.streamer.stopFallback();
564
+ }
565
+ }
566
+
567
+ class YouTubeExtractor {
568
+ static async downloadAudioToTempFile(youtubeUrl) {
569
+ return new Promise((resolve, reject) => {
570
+ const tempPath = path.join(os.tmpdir(), `yt-${Date.now()}-${Math.random().toString(36).substring(2, 10)}.webm`);
571
+
572
+ const metaProc = spawn('yt-dlp', [
573
+ youtubeUrl,
574
+ '--print', '%(url)s',
575
+ '--print', '%(title)s',
576
+ '--print', '%(duration)s',
577
+ '--print', '%(thumbnail)s',
578
+ '--no-playlist',
579
+ '--no-check-certificates',
580
+ '--quiet'
581
+ ], { timeout: 15000 });
582
+
583
+ let metaData = '';
584
+ metaProc.stdout.on('data', d => metaData += d.toString());
585
+ let metaErr = '';
586
+ metaProc.stderr.on('data', d => metaErr += d.toString());
587
+
588
+ metaProc.on('error', (err) => {
589
+ if (err.code === 'ENOENT') {
590
+ reject(new Error('yt-dlp not found. Install with: pip install "yt-dlp[default]"'));
591
+ } else {
592
+ reject(err);
593
+ }
594
+ });
595
+
596
+ metaProc.on('timeout', () => {
597
+ metaProc.kill('SIGKILL');
598
+ reject(new Error('Metadata fetch timed out (15s)'));
599
+ });
600
+
601
+ metaProc.on('close', async (code) => {
602
+ if (code !== 0) {
603
+ reject(new Error(`Failed to fetch YouTube metadata: ${metaErr || 'unknown error'} | Code: ${code}`));
604
+ return;
605
+ }
606
+
607
+ const lines = metaData.trim().split('\n');
608
+ if (lines.length < 4) {
609
+ reject(new Error('Invalid metadata response from yt-dlp'));
610
+ return;
611
+ }
612
+
613
+ const [, title, durStr, thumbnail] = lines;
614
+ const duration = parseInt(durStr) || 0;
615
+
616
+ const ytdlp = spawn('yt-dlp', [
617
+ youtubeUrl,
618
+ '-f', 'ba',
619
+ '--output', tempPath,
620
+ '--no-playlist',
621
+ '--no-check-certificates',
622
+ '--quiet'
623
+ ], { timeout: 30000 });
624
+
625
+ let stderr = '';
626
+ ytdlp.stderr.on('data', (chunk) => stderr += chunk.toString());
627
+
628
+ ytdlp.on('error', (err) => {
629
+ try { fs.unlink(tempPath); } catch { }
630
+ reject(err);
631
+ });
632
+
633
+ ytdlp.on('timeout', () => {
634
+ ytdlp.kill('SIGKILL');
635
+ try { fs.unlink(tempPath); } catch { }
636
+ reject(new Error('YouTube download timed out (30s)'));
637
+ });
638
+
639
+ ytdlp.on('close', async (dlCode) => {
640
+ if (dlCode !== 0) {
641
+ try { await fs.unlink(tempPath); } catch { }
642
+ reject(new Error(`yt-dlp download failed: ${stderr}`));
643
+ return;
644
+ }
645
+
646
+ resolve({
647
+ url: tempPath,
648
+ title: title || 'Unknown Track',
649
+ duration: duration,
650
+ thumbnail: thumbnail || null,
651
+ source: 'youtube',
652
+ isTempFile: true
653
+ });
654
+ });
655
+ });
656
+ });
657
+ }
658
+
659
+ static async searchAndDownload(query) {
660
+ const searchUrl = `ytsearch1:${query}`;
661
+ return await this.downloadAudioToTempFile(searchUrl);
662
+ }
663
+ }
664
+
665
+ class IcecastServer {
666
+ constructor(config = {}) {
667
+ this.app = express();
668
+ this.port = config.port || 3000;
669
+ this.config = config;
670
+ this.logger = config.logger || console;
671
+ this.setupExpress();
672
+ }
673
+
674
+ setupExpress() {
675
+ this.app.use(express.json());
676
+ this.app.get('/stream', (req, res) => {
677
+ res.redirect(this.config.publicStreamUrl);
678
+ });
679
+ }
680
+
681
+ start() {
682
+ this.app.listen(this.port, () => {
683
+ this.logger.success(`IcecastServer.start`, `running: http://localhost:${this.port}`);
684
+ this.logger.success(`IcecastServer.start`, `Icecast Stream: ${this.config.publicStreamUrl}`);
685
+ });
686
+ }
687
+ }
688
+
689
+ module.exports = {
690
+ IcecastStreamer,
691
+ IcecastQueue,
692
+ Track,
693
+ YouTubeExtractor,
694
+ IcecastServer
695
+ };