streamify-audio 2.1.15 → 2.1.16

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": "streamify-audio",
3
- "version": "2.1.15",
3
+ "version": "2.1.16",
4
4
  "description": "Dual-mode audio library: HTTP streaming proxy + Discord player (Lavalink alternative). Supports YouTube, Spotify, SoundCloud with audio filters.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -29,7 +29,8 @@
29
29
  "author": "Lucas",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "express": "^4.18.2"
32
+ "express": "^4.18.2",
33
+ "opusscript": "^0.0.8"
33
34
  },
34
35
  "optionalDependencies": {
35
36
  "@discordjs/opus": "^0.9.0",
@@ -217,7 +217,7 @@ class Manager extends EventEmitter {
217
217
  if (!this._isProviderEnabled('youtube')) {
218
218
  throw new Error('YouTube provider is disabled');
219
219
  }
220
- result = await youtube.search(query, limit, this.config);
220
+ result = await youtube.search(query, limit, this.config, options);
221
221
  break;
222
222
 
223
223
  case 'spotify':
@@ -218,6 +218,10 @@ class Player extends EventEmitter {
218
218
  });
219
219
 
220
220
  this.audioPlayer.on('error', (error) => {
221
+ if (this._manualSkip || this._changingStream) {
222
+ return;
223
+ }
224
+
221
225
  if (error.message === 'Premature close' || error.message.includes('EPIPE')) {
222
226
  return;
223
227
  }
@@ -234,7 +238,8 @@ class Player extends EventEmitter {
234
238
 
235
239
  const next = this.queue.shift();
236
240
  if (next) {
237
- this._playTrack(next);
241
+ // Use setImmediate to break recursion stack on consecutive failures
242
+ setImmediate(() => this._playTrack(next));
238
243
  } else {
239
244
  this.emit('queueEnd');
240
245
  }
@@ -309,7 +314,7 @@ class Player extends EventEmitter {
309
314
  };
310
315
 
311
316
  if (startPosition === 0 && this._prefetchedTrack?.id === track.id && this._prefetchedStream) {
312
- log.info('PLAYER', `Using prefetched stream for ${track.id}`);
317
+ log.debug('PLAYER', `Using prefetched stream for ${track.id}`);
313
318
  this.stream = this._prefetchedStream;
314
319
  this._prefetchedStream = null;
315
320
  this._prefetchedTrack = null;
@@ -332,10 +337,15 @@ class Player extends EventEmitter {
332
337
  } catch (error) {
333
338
  log.error('PLAYER', `Failed to play track: ${error.message}`);
334
339
  this.emit('trackError', track, error);
340
+
341
+ // If manual skip is active, we just return so the skip function handles it
342
+ if (this._manualSkip) return;
335
343
 
336
344
  const next = this.queue.shift();
337
345
  if (next) {
338
- return this._playTrack(next);
346
+ // Use setImmediate to break recursion stack on consecutive failures
347
+ setImmediate(() => this._playTrack(next));
348
+ return;
339
349
  } else {
340
350
  this.emit('queueEnd');
341
351
  }
@@ -351,7 +361,7 @@ class Player extends EventEmitter {
351
361
  this._prefetching = true;
352
362
  this._clearPrefetch();
353
363
 
354
- log.info('PLAYER', `Prefetching: ${nextTrack.title} (${nextTrack.id})`);
364
+ log.debug('PLAYER', `Prefetching: ${nextTrack.title} (${nextTrack.id})`);
355
365
 
356
366
  try {
357
367
  const filtersWithVolume = {
@@ -363,7 +373,7 @@ class Player extends EventEmitter {
363
373
  this._prefetchedStream = createStream(nextTrack, filtersWithVolume, this.config);
364
374
  await this._prefetchedStream.create();
365
375
 
366
- log.info('PLAYER', `Prefetch ready: ${nextTrack.id}`);
376
+ log.debug('PLAYER', `Prefetch ready: ${nextTrack.id}`);
367
377
  } catch (error) {
368
378
  log.debug('PLAYER', `Prefetch failed: ${error.message}`);
369
379
  this._clearPrefetch();
@@ -429,7 +439,7 @@ class Player extends EventEmitter {
429
439
  }
430
440
 
431
441
  this.audioPlayer.stop(true);
432
- log.info('PLAYER', `Paused at ${Math.floor(this._positionMs / 1000)}s (stream destroyed)`);
442
+ log.info('PLAYER', `Paused playback at ${Math.floor(this._positionMs / 1000)}s (Reason: User Request)`);
433
443
 
434
444
  return true;
435
445
  }
@@ -438,7 +448,7 @@ class Player extends EventEmitter {
438
448
  if (!this._playing || !this._paused) return false;
439
449
 
440
450
  if (!this.stream && this.queue.current) {
441
- log.info('PLAYER', `Resuming from ${Math.floor(this._positionMs / 1000)}s (recreating stream)`);
451
+ log.info('PLAYER', `Resuming playback from ${Math.floor(this._positionMs / 1000)}s`);
442
452
 
443
453
  this._changingStream = true;
444
454
  const track = this.queue.current;
@@ -488,18 +498,16 @@ class Player extends EventEmitter {
488
498
  const next = this.queue.shift();
489
499
  this.audioPlayer.stop();
490
500
 
491
- if (next) {
492
- await this._playTrack(next);
493
-
494
- await new Promise(resolve => setTimeout(resolve, 250));
501
+ log.info('PLAYER', `Skipping track (Next: ${next ? next.title : 'End of queue'})`);
495
502
 
496
- if (this.queue.current && this.queue.current.id !== next.id) {
497
- this._manualSkip = false;
498
- return this.queue.current;
503
+ try {
504
+ if (next) {
505
+ await this._playTrack(next);
499
506
  }
507
+ } finally {
508
+ this._manualSkip = false;
500
509
  }
501
510
 
502
- this._manualSkip = false;
503
511
  return this.queue.current || next;
504
512
  }
505
513
 
@@ -515,16 +523,15 @@ class Player extends EventEmitter {
515
523
 
516
524
  this._manualSkip = true;
517
525
  this.audioPlayer.stop();
518
- await this._playTrack(prev);
519
-
520
- await new Promise(resolve => setTimeout(resolve, 250));
526
+
527
+ log.info('PLAYER', `Rewinding to previous track: ${prev.title}`);
521
528
 
522
- if (this.queue.current && this.queue.current.id !== prev.id) {
529
+ try {
530
+ await this._playTrack(prev);
531
+ } finally {
523
532
  this._manualSkip = false;
524
- return this.queue.current;
525
533
  }
526
534
 
527
- this._manualSkip = false;
528
535
  return this.queue.current || prev;
529
536
  }
530
537
 
@@ -542,11 +549,16 @@ class Player extends EventEmitter {
542
549
  this._positionMs = 0;
543
550
  this.queue.clear();
544
551
  this.queue.setCurrent(null);
552
+
553
+ log.info('PLAYER', 'Stopped playback and cleared queue');
554
+
545
555
  return true;
546
556
  }
547
557
 
548
558
  setVolume(volume) {
559
+ const oldVolume = this._volume;
549
560
  this._volume = Math.max(0, Math.min(200, volume));
561
+ log.info('PLAYER', `Volume adjusted: ${oldVolume}% -> ${this._volume}%`);
550
562
  return this._volume;
551
563
  }
552
564
 
@@ -565,6 +577,8 @@ class Player extends EventEmitter {
565
577
  this.stream.destroy();
566
578
  }
567
579
 
580
+ log.info('PLAYER', `Seeking to ${Math.floor(positionMs / 1000)}s`);
581
+
568
582
  try {
569
583
  this.stream = createStream(track, filtersWithVolume, this.config);
570
584
  const resource = await this.stream.create(positionMs);
@@ -580,6 +594,7 @@ class Player extends EventEmitter {
580
594
  }
581
595
 
582
596
  setLoop(mode) {
597
+ log.info('PLAYER', `Loop mode changed to: ${mode.toUpperCase()}`);
583
598
  return this.queue.setRepeatMode(mode);
584
599
  }
585
600
 
@@ -617,6 +632,7 @@ class Player extends EventEmitter {
617
632
 
618
633
  async clearFilters() {
619
634
  this._filters = {};
635
+ log.info('PLAYER', 'All audio filters cleared');
620
636
  if (this._playing && this.queue.current) {
621
637
  return this.setFilter('_trigger', null);
622
638
  }
@@ -627,6 +643,7 @@ class Player extends EventEmitter {
627
643
  if (!Array.isArray(bands) || bands.length !== 15) {
628
644
  throw new Error('EQ must be an array of 15 band gains (-0.25 to 1.0)');
629
645
  }
646
+ log.info('PLAYER', 'Applying custom 15-band Equalizer');
630
647
  return this.setFilter('equalizer', bands);
631
648
  }
632
649
 
@@ -636,12 +653,14 @@ class Player extends EventEmitter {
636
653
  throw new Error(`Unknown preset: ${presetName}. Available: ${Object.keys(PRESETS).join(', ')}`);
637
654
  }
638
655
  delete this._filters.equalizer;
656
+ log.info('PLAYER', `Applying EQ preset: ${presetName.toUpperCase()}`);
639
657
  return this.setFilter('preset', presetName);
640
658
  }
641
659
 
642
660
  async clearEQ() {
643
661
  delete this._filters.equalizer;
644
662
  delete this._filters.preset;
663
+ log.info('PLAYER', 'Equalizer cleared');
645
664
  if (this._playing && this.queue.current) {
646
665
  return this.setFilter('_trigger', null);
647
666
  }
@@ -659,7 +678,6 @@ class Player extends EventEmitter {
659
678
  }
660
679
 
661
680
  const replace = options.replace ?? false;
662
- const appliedFilters = {};
663
681
  const newPresets = [];
664
682
 
665
683
  for (const preset of presets) {
@@ -670,17 +688,11 @@ class Player extends EventEmitter {
670
688
  log.warn('PLAYER', `Unknown effect preset: ${name}`);
671
689
  continue;
672
690
  }
673
-
674
- const filters = applyEffectPreset(name, intensity);
675
- if (filters) {
676
- Object.assign(appliedFilters, filters);
677
- newPresets.push({ name, intensity });
678
- }
691
+ newPresets.push({ name, intensity });
679
692
  }
680
693
 
681
694
  if (replace) {
682
695
  this._effectPresets = newPresets;
683
- this._filters = appliedFilters;
684
696
  } else {
685
697
  const existingNames = this._effectPresets.map(p => p.name);
686
698
  for (const preset of newPresets) {
@@ -691,9 +703,19 @@ class Player extends EventEmitter {
691
703
  this._effectPresets.push(preset);
692
704
  }
693
705
  }
694
- this._filters = { ...this._filters, ...appliedFilters };
695
706
  }
696
707
 
708
+ // Re-calculate all filters from active presets
709
+ this._filters = {};
710
+ for (const preset of this._effectPresets) {
711
+ const filters = applyEffectPreset(preset.name, preset.intensity);
712
+ if (filters) {
713
+ Object.assign(this._filters, filters);
714
+ }
715
+ }
716
+
717
+ log.info('PLAYER', `Active effects: ${this._effectPresets.map(p => p.name).join(' + ') || 'NONE'}`);
718
+
697
719
  if (this._playing && this.queue.current) {
698
720
  return this.setFilter('_trigger', null);
699
721
  }
@@ -707,6 +729,7 @@ class Player extends EventEmitter {
707
729
  async clearEffectPresets() {
708
730
  this._effectPresets = [];
709
731
  this._filters = {};
732
+ log.info('PLAYER', 'All effect presets cleared');
710
733
  if (this._playing && this.queue.current) {
711
734
  return this.setFilter('_trigger', null);
712
735
  }
@@ -779,4 +802,4 @@ class Player extends EventEmitter {
779
802
  }
780
803
  }
781
804
 
782
- module.exports = Player;
805
+ module.exports = Player;
@@ -22,6 +22,17 @@ class StreamController {
22
22
  this.destroyed = false;
23
23
  this.startTime = null;
24
24
  this.bytesReceived = 0;
25
+ this.bytesSent = 0;
26
+ this.ytdlpError = '';
27
+ this.ffmpegError = '';
28
+
29
+ // Timing metrics
30
+ this.metrics = {
31
+ metadata: 0,
32
+ spawn: 0,
33
+ firstByte: 0,
34
+ total: 0
35
+ };
25
36
  }
26
37
 
27
38
  async create(seekPosition = 0) {
@@ -34,6 +45,7 @@ class StreamController {
34
45
  }
35
46
 
36
47
  this.startTime = Date.now();
48
+ const startTimestamp = this.startTime;
37
49
  const source = this.track.source || 'youtube';
38
50
 
39
51
  let videoId = this.track._resolvedId || this.track.id;
@@ -44,10 +56,13 @@ class StreamController {
44
56
  const spotify = require('../providers/spotify');
45
57
  videoId = await spotify.resolveToYouTube(this.track.id, this.config);
46
58
  this.track._resolvedId = videoId;
59
+ this.metrics.metadata = Date.now() - startTimestamp;
47
60
  } catch (error) {
48
61
  log.error('STREAM', `Spotify resolution failed: ${error.message}`);
49
62
  throw new Error(`Failed to resolve Spotify track: ${error.message}`);
50
63
  }
64
+ } else {
65
+ this.metrics.metadata = Date.now() - startTimestamp;
51
66
  }
52
67
 
53
68
  if (!videoId || videoId === 'undefined') {
@@ -55,6 +70,17 @@ class StreamController {
55
70
  }
56
71
 
57
72
  log.info('STREAM', `Creating stream for ${videoId} (${source})`);
73
+
74
+ // Log Filter Chain
75
+ const filterNames = Object.keys(this.filters).filter(k => k !== 'start' && k !== '_trigger');
76
+ if (filterNames.length > 0) {
77
+ const chain = filterNames.map(name => {
78
+ const val = this.filters[name];
79
+ const displayVal = typeof val === 'object' ? JSON.stringify(val) : val;
80
+ return `[${name} (${displayVal})]`;
81
+ }).join(' -> ');
82
+ log.debug('STREAM', `Filter Chain: ${chain}`);
83
+ }
58
84
 
59
85
  let url;
60
86
  if (source === 'soundcloud') {
@@ -106,35 +132,45 @@ class StreamController {
106
132
  ytdlpArgs.push('--sponsorblock-remove', categories.join(','));
107
133
  }
108
134
 
109
- this.ytdlp = spawn(this.config.ytdlpPath, ytdlpArgs, {
110
- env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
111
- });
135
+ const env = { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH };
112
136
 
113
- this._spawnTime = Date.now() - this.startTime;
137
+ const spawnStart = Date.now();
138
+ this.ytdlp = spawn(this.config.ytdlpPath, ytdlpArgs, { env });
114
139
 
115
140
  const ffmpegFilters = { ...this.filters };
116
- if (seekPosition > 0) {
117
- ffmpegFilters.start = seekPosition / 1000;
118
- }
119
-
120
141
  const ffmpegArgs = buildFfmpegArgs(ffmpegFilters, this.config);
121
- this.ffmpeg = spawn(this.config.ffmpegPath, ffmpegArgs);
142
+ this.ffmpeg = spawn(this.config.ffmpegPath, ffmpegArgs, { env });
143
+
144
+ this.metrics.spawn = Date.now() - spawnStart;
145
+
146
+ const { pipeline } = require('stream');
122
147
 
123
148
  this._firstDataTime = null;
149
+
124
150
  this.ytdlp.stdout.on('data', (chunk) => {
151
+ this.bytesReceived += chunk.length;
152
+ });
153
+
154
+ this.ffmpeg.stdout.on('data', (chunk) => {
125
155
  if (!this._firstDataTime) {
126
156
  this._firstDataTime = Date.now() - this.startTime;
157
+ this.metrics.firstByte = Date.now() - (startTimestamp + this.metrics.metadata + this.metrics.spawn);
127
158
  }
128
- this.bytesReceived += chunk.length;
159
+ this.bytesSent += chunk.length;
129
160
  });
130
161
 
131
- this.ytdlp.stdout.pipe(this.ffmpeg.stdin);
162
+ pipeline(this.ytdlp.stdout, this.ffmpeg.stdin, (err) => {
163
+ if (err && !this.destroyed) {
164
+ if (err.code !== 'ERR_STREAM_PREMATURE_CLOSE' && err.code !== 'EPIPE') {
165
+ log.debug('STREAM', `Pipeline error: ${err.message}`);
166
+ }
167
+ }
168
+ });
132
169
 
133
- let ytdlpError = '';
134
170
  this.ytdlp.stderr.on('data', (data) => {
135
171
  if (this.destroyed) return;
136
172
  const msg = data.toString();
137
- ytdlpError += msg;
173
+ this.ytdlpError += msg;
138
174
  if (msg.includes('ERROR:') && !msg.includes('Retrying') && !msg.includes('Broken pipe')) {
139
175
  log.error('YTDLP', msg.trim());
140
176
  } else if (!msg.includes('[download]') && !msg.includes('ETA') && !msg.includes('[youtube]') && !msg.includes('Retrying fragment') && !msg.includes('Got error')) {
@@ -145,6 +181,7 @@ class StreamController {
145
181
  this.ffmpeg.stderr.on('data', (data) => {
146
182
  if (this.destroyed) return;
147
183
  const msg = data.toString();
184
+ this.ffmpegError += msg;
148
185
  if ((msg.includes('Error') || msg.includes('error')) && !msg.includes('Connection reset') && !msg.includes('Broken pipe')) {
149
186
  log.error('FFMPEG', msg.trim());
150
187
  }
@@ -153,11 +190,11 @@ class StreamController {
153
190
  this.ytdlp.on('close', (code) => {
154
191
  if (code !== 0 && code !== null && !this.destroyed) {
155
192
  log.error('STREAM', `yt-dlp failed (code: ${code}) for ${videoId}`);
156
- if (ytdlpError) {
157
- log.error('STREAM', `yt-dlp stderr: ${ytdlpError.slice(-500)}`);
193
+ if (this.ytdlpError) {
194
+ log.error('STREAM', `yt-dlp stderr: ${this.ytdlpError.slice(-500)}`);
158
195
  }
159
196
  }
160
- if (this.ffmpeg && !this.ffmpeg.killed) {
197
+ if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.stdin) {
161
198
  this.ffmpeg.stdin.end();
162
199
  }
163
200
  });
@@ -199,9 +236,10 @@ class StreamController {
199
236
  inlineVolume: false
200
237
  });
201
238
 
202
- const elapsed = Date.now() - this.startTime;
203
- const firstData = this._firstDataTime || elapsed;
204
- log.info('STREAM', `Ready ${elapsed}ms | First byte: ${firstData}ms | Buffered: ${this.bytesReceived}`);
239
+ const elapsed = Date.now() - startTimestamp;
240
+ this.metrics.total = elapsed;
241
+
242
+ log.info('STREAM', `Ready ${elapsed}ms | Metrics: [Metadata: ${this.metrics.metadata}ms | Spawn: ${this.metrics.spawn}ms | FirstByte: ${this.metrics.firstByte}ms] | Buffered: ${this.bytesSent}b`);
205
243
 
206
244
  return this.resource;
207
245
  }
@@ -210,7 +248,7 @@ class StreamController {
210
248
  return new Promise((resolve, reject) => {
211
249
  const timeoutMs = isLive ? 30000 : 15000;
212
250
  const timeout = setTimeout(() => {
213
- log.warn('STREAM', `Timeout waiting for data, proceeding anyway (received: ${this.bytesReceived}, isLive: ${isLive})`);
251
+ log.warn('STREAM', `Timeout waiting for data, proceeding anyway (received: ${this.bytesSent}, isLive: ${isLive})`);
214
252
  resolve();
215
253
  }, timeoutMs);
216
254
 
@@ -221,7 +259,7 @@ class StreamController {
221
259
  clearInterval(checkInterval);
222
260
  return;
223
261
  }
224
- if (this.bytesReceived > 0) {
262
+ if (this.bytesSent > 0) {
225
263
  resolved = true;
226
264
  clearTimeout(timeout);
227
265
  clearInterval(checkInterval);
@@ -234,7 +272,7 @@ class StreamController {
234
272
  resolved = true;
235
273
  clearTimeout(timeout);
236
274
  clearInterval(checkInterval);
237
- reject(new Error('ffmpeg closed before producing data'));
275
+ reject(new Error(`ffmpeg closed before producing data. ffmpeg stderr: ${this.ffmpegError.slice(-200) || 'none'} | yt-dlp stderr: ${this.ytdlpError.slice(-200) || 'none'}`));
238
276
  }
239
277
  });
240
278
 
@@ -243,7 +281,7 @@ class StreamController {
243
281
  resolved = true;
244
282
  clearTimeout(timeout);
245
283
  clearInterval(checkInterval);
246
- reject(new Error(`yt-dlp failed with code ${code}`));
284
+ reject(new Error(`yt-dlp failed with code ${code}. stderr: ${this.ytdlpError.slice(-200) || 'none'}`));
247
285
  }
248
286
  });
249
287
  });
@@ -254,7 +292,7 @@ class StreamController {
254
292
  this.destroyed = true;
255
293
 
256
294
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;
257
- log.info('STREAM', `Destroying stream | Duration: ${elapsed}ms | Received: ${this.bytesReceived}`);
295
+ log.info('STREAM', `Destroying stream | Duration: ${elapsed}ms | Data Out: ${(this.bytesSent / 1024 / 1024).toFixed(2)} MB`);
258
296
 
259
297
  try {
260
298
  if (this.ytdlp && this.ffmpeg) {
@@ -287,4 +325,4 @@ function createStream(track, filters, config) {
287
325
  return new StreamController(track, filters, config);
288
326
  }
289
327
 
290
- module.exports = { createStream, StreamController };
328
+ module.exports = { createStream, StreamController };
@@ -15,27 +15,38 @@ const PRESETS = {
15
15
  piano: [0.2, 0.15, 0.1, 0.05, 0, 0.05, 0.1, 0.15, 0.2, 0.2, 0.15, 0.1, 0.1, 0.15, 0.2],
16
16
  vocal: [-0.2, -0.15, -0.1, 0, 0.2, 0.35, 0.4, 0.4, 0.35, 0.2, 0, -0.1, -0.15, -0.15, -0.1],
17
17
  bass_heavy: [0.5, 0.45, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
18
- treble_heavy: [0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5, 0.5]
18
+ treble_heavy: [0, 0, 0, 0, 0, 0, 0, 0, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5, 0.5],
19
+ extra_bass: [0.6, 0.55, 0.5, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0],
20
+ crystal_clear: [-0.1, -0.1, -0.05, 0, 0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.5, 0.5, 0.5]
19
21
  };
20
22
 
21
23
  const EFFECT_PRESETS = {
22
- bassboost: { filters: { bass: 10 }, description: 'Boost bass frequencies' },
24
+ bassboost: { filters: { bass: 12, equalizer: [0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, description: 'Heavy bass boost' },
23
25
  nightcore: { filters: { speed: 1.25, pitch: 1.25 }, description: 'Speed up with higher pitch' },
24
26
  vaporwave: { filters: { speed: 0.8, pitch: 0.8 }, description: 'Slow down with lower pitch' },
25
- '8d': { filters: { rotation: { speed: 0.2 } }, description: '8D rotating audio effect' },
27
+ '8d': { filters: { rotation: { speed: 0.15 } }, description: '8D rotating audio effect' },
26
28
  karaoke: { filters: { karaoke: true }, description: 'Reduce vocals' },
27
29
  trebleboost: { filters: { treble: 10 }, description: 'Boost treble frequencies' },
28
- deep: { filters: { bass: 15, pitch: 0.9 }, description: 'Deep bass with lower pitch' },
29
- lofi: { filters: { lowpass: 3000, bass: 5 }, description: 'Lo-fi aesthetic' },
30
- radio: { filters: { highpass: 300, lowpass: 5000 }, description: 'Radio/telephone effect' },
31
- telephone: { filters: { highpass: 500, lowpass: 3500 }, description: 'Old telephone effect' },
32
- soft: { filters: { bass: -5, treble: -3, volume: 70 }, description: 'Softer, quieter sound' },
33
- loud: { filters: { bass: 5, treble: 3, compressor: true }, description: 'Louder, compressed sound' },
34
- chipmunk: { filters: { pitch: 1.5 }, description: 'High-pitched chipmunk voice' },
35
- darth: { filters: { pitch: 0.7 }, description: 'Deep Darth Vader voice' },
30
+ deep: { filters: { bass: 15, pitch: 0.85, equalizer: [0.5, 0.4, 0.3, 0.2, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, description: 'Deep bass with lower pitch' },
31
+ lofi: { filters: { lowpass: 2500, bass: 8, treble: -5 }, description: 'Lo-fi aesthetic' },
32
+ radio: { filters: { highpass: 400, lowpass: 4500, compressor: true }, description: 'Radio/telephone effect' },
33
+ telephone: { filters: { highpass: 600, lowpass: 3000, compressor: true }, description: 'Old telephone effect' },
34
+ soft: { filters: { bass: -3, treble: -5, volume: 80, compressor: true }, description: 'Softer, compressed sound' },
35
+ loud: { filters: { bass: 3, treble: 2, volume: 120, compressor: true, normalizer: true }, description: 'Louder, punchier sound' },
36
+ chipmunk: { filters: { pitch: 1.6 }, description: 'High-pitched chipmunk voice' },
37
+ darth: { filters: { pitch: 0.65 }, description: 'Deep Darth Vader voice' },
36
38
  echo: { filters: { echo: true }, description: 'Echo/reverb effect' },
37
- vibrato: { filters: { vibrato: { frequency: 5, depth: 0.5 } }, description: 'Vibrato effect' },
38
- tremolo: { filters: { tremolo: { frequency: 5, depth: 0.5 } }, description: 'Tremolo effect' }
39
+ vibrato: { filters: { vibrato: { frequency: 6, depth: 0.6 } }, description: 'Vibrato effect' },
40
+ tremolo: { filters: { tremolo: { frequency: 5, depth: 0.5 } }, description: 'Tremolo effect' },
41
+ reverb: { filters: { reverb: true }, description: 'Classic reverb effect' },
42
+ surround: { filters: { surround: true }, description: 'Surround sound effect' },
43
+ boost: { filters: { volume: 150, treble: 5, bass: 5, compressor: true }, description: 'Boost volume and clarity' },
44
+ subboost: { filters: { bass: 15, lowpass: 100 }, description: 'Extreme sub-woofer boost' },
45
+ pop: { filters: { preset: 'pop' }, description: 'Pop EQ preset' },
46
+ rock: { filters: { preset: 'rock' }, description: 'Rock EQ preset' },
47
+ electronic: { filters: { preset: 'electronic' }, description: 'Electronic EQ preset' },
48
+ jazz: { filters: { preset: 'jazz' }, description: 'Jazz EQ preset' },
49
+ classical: { filters: { preset: 'classical' }, description: 'Classical EQ preset' }
39
50
  };
40
51
 
41
52
  function buildEqualizer(bands) {
@@ -84,11 +95,7 @@ function buildFfmpegArgs(filters = {}, config = {}) {
84
95
  const speed = parseFloat(filters.speed);
85
96
  if (!isNaN(speed) && speed !== 1) {
86
97
  const clampedSpeed = Math.max(0.5, Math.min(2.0, speed));
87
- if (clampedSpeed < 1) {
88
- audioFilters.push(`atempo=${clampedSpeed}`);
89
- } else if (clampedSpeed <= 2) {
90
- audioFilters.push(`atempo=${clampedSpeed}`);
91
- }
98
+ audioFilters.push(`atempo=${clampedSpeed}`);
92
99
  }
93
100
 
94
101
  const pitch = parseFloat(filters.pitch);
@@ -205,18 +212,24 @@ function buildFfmpegArgs(filters = {}, config = {}) {
205
212
  audioFilters.push('loudnorm');
206
213
  }
207
214
 
215
+ if (filters.echo === 'true' || filters.echo === true) {
216
+ audioFilters.push('aecho=0.8:0.88:60:0.4');
217
+ }
218
+
219
+ if (filters.reverb === 'true' || filters.reverb === true) {
220
+ audioFilters.push('aecho=0.8:0.88:60:0.4,aecho=0.8:0.88:30:0.2');
221
+ }
222
+
208
223
  if (filters.nightcore === 'true' || filters.nightcore === true) {
209
- audioFilters.push('atempo=1.25');
210
- audioFilters.push('asetrate=48000*1.25,aresample=48000');
224
+ audioFilters.push('atempo=1.25,asetrate=48000*1.25,aresample=48000');
211
225
  }
212
226
 
213
227
  if (filters.vaporwave === 'true' || filters.vaporwave === true) {
214
- audioFilters.push('atempo=0.8');
215
- audioFilters.push('asetrate=48000*0.8,aresample=48000');
228
+ audioFilters.push('atempo=0.8,asetrate=48000*0.8,aresample=48000');
216
229
  }
217
230
 
218
231
  if (filters.bassboost === 'true' || filters.bassboost === true) {
219
- audioFilters.push('bass=g=10');
232
+ audioFilters.push('bass=g=10,equalizer=f=40:width_type=h:width=20:g=10');
220
233
  }
221
234
 
222
235
  if (filters['8d'] === 'true' || filters['8d'] === true) {
@@ -275,6 +288,8 @@ function getAvailableFilters() {
275
288
  nightcore: { type: 'boolean', description: 'Nightcore preset' },
276
289
  vaporwave: { type: 'boolean', description: 'Vaporwave preset' },
277
290
  bassboost: { type: 'boolean', description: 'Bass boost preset' },
291
+ echo: { type: 'boolean', description: 'Echo effect' },
292
+ reverb: { type: 'boolean', description: 'Reverb effect' },
278
293
  '8d': { type: 'boolean', description: '8D audio effect' }
279
294
  };
280
295
  }
@@ -306,14 +321,24 @@ function applyEffectPreset(name, intensity = 1.0) {
306
321
  const filters = {};
307
322
  for (const [key, value] of Object.entries(preset.filters)) {
308
323
  if (typeof value === 'number') {
309
- filters[key] = value * intensity;
324
+ if (key === 'speed' || key === 'pitch') {
325
+ // Scale relative to 1.0
326
+ filters[key] = 1.0 + (value - 1.0) * intensity;
327
+ } else {
328
+ filters[key] = value * intensity;
329
+ }
310
330
  } else if (typeof value === 'boolean') {
311
331
  filters[key] = value;
312
332
  } else if (typeof value === 'object') {
313
333
  filters[key] = { ...value };
314
334
  for (const [k, v] of Object.entries(filters[key])) {
315
335
  if (typeof v === 'number') {
316
- filters[key][k] = v * intensity;
336
+ if (k === 'speed' || k === 'pitch' || k === 'frequency') {
337
+ // Some frequency based ones might also need base values but speed/pitch are most critical
338
+ filters[key][k] = 1.0 + (v - 1.0) * intensity;
339
+ } else {
340
+ filters[key][k] = v * intensity;
341
+ }
317
342
  }
318
343
  }
319
344
  }
@@ -332,4 +357,4 @@ module.exports = {
332
357
  PRESETS,
333
358
  EQ_BANDS,
334
359
  EFFECT_PRESETS
335
- };
360
+ };
@@ -3,23 +3,42 @@ const { buildFfmpegArgs } = require('../filters/ffmpeg');
3
3
  const { registerStream, unregisterStream } = require('../utils/stream');
4
4
  const log = require('../utils/logger');
5
5
 
6
- async function search(query, limit, config) {
6
+ async function search(query, limit, config, options = {}) {
7
7
  const startTime = Date.now();
8
- log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit})`);
8
+ log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit}, type: ${options.type || 'all'}, sort: ${options.sort || 'relevance'})`);
9
9
 
10
10
  return new Promise((resolve, reject) => {
11
11
  const args = [
12
12
  '-q', '--no-warnings',
13
13
  '--flat-playlist',
14
14
  '--skip-download',
15
- '-J',
16
- `ytsearch${limit}:${query}`
15
+ '-J'
17
16
  ];
18
17
 
19
18
  if (config.cookiesPath) {
20
19
  args.unshift('--cookies', config.cookiesPath);
21
20
  }
22
21
 
22
+ let searchQuery = query;
23
+
24
+ // Handle sorting by modifying search query or using filters
25
+ if (options.sort === 'views' || options.sort === 'popular') {
26
+ searchQuery += ' most viewed';
27
+ } else if (options.sort === 'date' || options.sort === 'latest') {
28
+ searchQuery += ' new';
29
+ }
30
+
31
+ // Handle type filtering
32
+ if (options.type === 'live') {
33
+ args.push('--match-filter', 'live_status = is_live');
34
+ } else if (options.type === 'playlist') {
35
+ args.push('--match-filter', 'playlist_count > 0');
36
+ } else if (options.type === 'video') {
37
+ args.push('--match-filter', 'live_status != is_live');
38
+ }
39
+
40
+ args.push(`ytsearch${limit}:${searchQuery}`);
41
+
23
42
  const proc = spawn(config.ytdlpPath, args, {
24
43
  env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
25
44
  });
package/src/server.js CHANGED
@@ -93,14 +93,14 @@ class Server {
93
93
  if (!this._isProviderEnabled('youtube')) {
94
94
  return res.status(400).json({ error: 'YouTube provider is disabled' });
95
95
  }
96
- const { q, limit = 10 } = req.query;
96
+ const { q, limit = 10, type, sort } = req.query;
97
97
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
98
98
 
99
- const cacheKey = `yt:search:${q}:${limit}`;
99
+ const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}`;
100
100
  const cached = cache.get(cacheKey);
101
101
  if (cached) return res.json(cached);
102
102
 
103
- const results = await youtube.search(q, parseInt(limit), this.config);
103
+ const results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
104
104
  cache.set(cacheKey, results, this.config.cache.searchTTL);
105
105
  res.json(results);
106
106
  } catch (error) {
@@ -228,7 +228,7 @@ class Server {
228
228
 
229
229
  this.app.get('/search', async (req, res) => {
230
230
  try {
231
- const { q, source = 'youtube', limit = 10 } = req.query;
231
+ const { q, source = 'youtube', limit = 10, type, sort } = req.query;
232
232
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
233
233
 
234
234
  let results;
@@ -252,7 +252,7 @@ class Server {
252
252
  if (!this._isProviderEnabled('youtube')) {
253
253
  return res.status(400).json({ error: 'YouTube provider is disabled' });
254
254
  }
255
- results = await youtube.search(q, parseInt(limit), this.config);
255
+ results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
256
256
  }
257
257
  res.json(results);
258
258
  } catch (error) {
@@ -52,45 +52,67 @@ function formatTime() {
52
52
  }
53
53
 
54
54
  function formatTag(tag, color) {
55
- return colorize(color, `[${tag}]`);
55
+ return colorize(color, `[${tag.toUpperCase()}]`);
56
56
  }
57
57
 
58
58
  function debug(tag, ...args) {
59
59
  if (currentLevel < LOG_LEVELS.debug) return;
60
- console.log(formatTime(), formatTag(tag, 'gray'), colorize('dim', args.join(' ')));
60
+ console.log(formatTime(), '🐛', formatTag(tag, 'gray'), colorize('dim', args.join(' ')));
61
61
  }
62
62
 
63
63
  function info(tag, ...args) {
64
64
  if (currentLevel < LOG_LEVELS.info) return;
65
- console.log(formatTime(), formatTag(tag, 'cyan'), ...args);
65
+ const tagUpper = tag.toUpperCase();
66
+ const color = {
67
+ 'MANAGER': 'cyan',
68
+ 'PLAYER': 'blue',
69
+ 'YOUTUBE': 'red',
70
+ 'SPOTIFY': 'green',
71
+ 'SOUNDCLOUD': 'yellow',
72
+ 'STREAM': 'cyan'
73
+ }[tagUpper] || 'cyan';
74
+
75
+ const emojiMap = {
76
+ 'MANAGER': 'â„šī¸ ',
77
+ 'YOUTUBE': '🔴 ',
78
+ 'SPOTIFY': 'đŸŸĸ ',
79
+ 'SOUNDCLOUD': '🟠 ',
80
+ 'PLAYER': '🔹 ',
81
+ 'STREAM': 'đŸŽĩ '
82
+ };
83
+
84
+ const emoji = emojiMap[tagUpper] || ' ';
85
+ console.log(formatTime(), emoji, formatTag(tag, color), ...args);
66
86
  }
67
87
 
68
88
  function success(tag, ...args) {
69
89
  if (currentLevel < LOG_LEVELS.info) return;
70
- console.log(formatTime(), formatTag(tag, 'green'), colorize('green', args.join(' ')));
90
+ console.log(formatTime(), '✅ ', formatTag(tag, 'green'), colorize('green', args.join(' ')));
71
91
  }
72
92
 
73
93
  function warn(tag, ...args) {
74
94
  if (currentLevel < LOG_LEVELS.warn) return;
75
- console.warn(formatTime(), formatTag(tag, 'yellow'), colorize('yellow', args.join(' ')));
95
+ console.warn(formatTime(), 'âš ī¸ ', formatTag(tag, 'yellow'), colorize('yellow', args.join(' ')));
76
96
  }
77
97
 
78
98
  function error(tag, ...args) {
79
99
  if (currentLevel < LOG_LEVELS.error) return;
80
- console.error(formatTime(), formatTag(tag, 'red'), colorize('red', args.join(' ')));
100
+ console.error(formatTime(), '❌ ', formatTag(tag, 'red'), colorize('red', args.join(' ')));
81
101
  }
82
102
 
83
103
  function stream(source, id, message) {
84
104
  if (currentLevel < LOG_LEVELS.debug) return;
85
- const sourceColor = {
86
- youtube: 'red',
87
- spotify: 'green',
88
- soundcloud: 'yellow'
89
- }[source] || 'white';
105
+ const sourceMap = {
106
+ youtube: { color: 'red', emoji: '🔴 ' },
107
+ spotify: { color: 'green', emoji: 'đŸŸĸ ' },
108
+ soundcloud: { color: 'yellow', emoji: '🟠 ' }
109
+ };
110
+ const { color, emoji } = sourceMap[source.toLowerCase()] || { color: 'white', emoji: 'đŸŽĩ ' };
90
111
 
91
112
  console.log(
92
113
  formatTime(),
93
- formatTag('STREAM', sourceColor),
114
+ emoji,
115
+ colorize('cyan', '[STREAM]'),
94
116
  colorize('dim', `[${id}]`),
95
117
  message
96
118
  );