streamify-audio 2.2.2 → 2.2.6

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 (41) hide show
  1. package/.github/workflows/npm-publish-github-packages.yml +36 -0
  2. package/README.md +81 -7
  3. package/docs/configuration.md +31 -2
  4. package/docs/discord/manager.md +14 -7
  5. package/docs/discord/player.md +84 -1
  6. package/docs/filters.md +39 -1
  7. package/docs/http/endpoints.md +25 -0
  8. package/docs/http/server.md +2 -0
  9. package/docs/sources.md +52 -4
  10. package/index.d.ts +31 -1
  11. package/index.js +67 -3
  12. package/package.json +8 -6
  13. package/src/cache/index.js +2 -1
  14. package/src/config.js +18 -1
  15. package/src/discord/Manager.js +101 -7
  16. package/src/discord/Player.js +281 -99
  17. package/src/discord/Stream.js +204 -137
  18. package/src/filters/ffmpeg.js +124 -15
  19. package/src/providers/bandcamp.js +49 -0
  20. package/src/providers/http.js +35 -0
  21. package/src/providers/local.js +36 -0
  22. package/src/providers/mixcloud.js +49 -0
  23. package/src/providers/soundcloud.js +5 -1
  24. package/src/providers/twitch.js +49 -0
  25. package/src/providers/youtube.js +58 -17
  26. package/src/server.js +60 -5
  27. package/src/utils/logger.js +34 -12
  28. package/tests/cache.test.js +234 -0
  29. package/tests/config.test.js +44 -0
  30. package/tests/error-handling.test.js +318 -0
  31. package/tests/ffmpeg.test.js +66 -0
  32. package/tests/filters-edge.test.js +333 -0
  33. package/tests/http.test.js +24 -0
  34. package/tests/integration.test.js +325 -0
  35. package/tests/local.test.js +37 -0
  36. package/tests/queue.test.js +94 -0
  37. package/tests/spotify.test.js +238 -0
  38. package/tests/stream.test.js +217 -0
  39. package/tests/twitch.test.js +42 -0
  40. package/tests/utils.test.js +60 -0
  41. package/tests/youtube.test.js +219 -0
package/index.js CHANGED
@@ -100,7 +100,7 @@ class Streamify extends EventEmitter {
100
100
  async search(source, query, limit = 10) {
101
101
  if (!this.running) throw new Error('Streamify not started. Call .start() first');
102
102
 
103
- switch (source) {
103
+ switch (source.toLowerCase()) {
104
104
  case 'youtube':
105
105
  case 'yt':
106
106
  return youtube.search(query, limit, this.config);
@@ -110,6 +110,21 @@ class Streamify extends EventEmitter {
110
110
  case 'soundcloud':
111
111
  case 'sc':
112
112
  return soundcloud.search(query, limit, this.config);
113
+ case 'twitch':
114
+ const twitch = require('./src/providers/twitch');
115
+ return { tracks: [await twitch.getInfo(query, this.config)], source: 'twitch' };
116
+ case 'mixcloud':
117
+ const mixcloud = require('./src/providers/mixcloud');
118
+ return { tracks: [await mixcloud.getInfo(query, this.config)], source: 'mixcloud' };
119
+ case 'bandcamp':
120
+ const bandcamp = require('./src/providers/bandcamp');
121
+ return { tracks: [await bandcamp.getInfo(query, this.config)], source: 'bandcamp' };
122
+ case 'http':
123
+ const http = require('./src/providers/http');
124
+ return { tracks: [await http.getInfo(query, this.config)], source: 'http' };
125
+ case 'local':
126
+ const local = require('./src/providers/local');
127
+ return { tracks: [await local.getInfo(query, this.config)], source: 'local' };
113
128
  default:
114
129
  throw new Error(`Unknown source: ${source}`);
115
130
  }
@@ -118,13 +133,26 @@ class Streamify extends EventEmitter {
118
133
  async getInfo(source, id) {
119
134
  if (!this.running) throw new Error('Streamify not started. Call .start() first');
120
135
 
121
- switch (source) {
136
+ switch (source.toLowerCase()) {
122
137
  case 'youtube':
123
138
  case 'yt':
124
139
  return youtube.getInfo(id, this.config);
125
140
  case 'spotify':
126
141
  case 'sp':
127
142
  return spotify.getInfo(id, this.config);
143
+ case 'soundcloud':
144
+ case 'sc':
145
+ return soundcloud.getInfo(id, this.config);
146
+ case 'twitch':
147
+ return require('./src/providers/twitch').getInfo(id, this.config);
148
+ case 'mixcloud':
149
+ return require('./src/providers/mixcloud').getInfo(id, this.config);
150
+ case 'bandcamp':
151
+ return require('./src/providers/bandcamp').getInfo(id, this.config);
152
+ case 'http':
153
+ return require('./src/providers/http').getInfo(id, this.config);
154
+ case 'local':
155
+ return require('./src/providers/local').getInfo(id, this.config);
128
156
  default:
129
157
  throw new Error(`Unknown source: ${source}`);
130
158
  }
@@ -134,7 +162,7 @@ class Streamify extends EventEmitter {
134
162
  if (!this.config) throw new Error('Streamify not started. Call .start() first');
135
163
 
136
164
  let endpoint;
137
- switch (source) {
165
+ switch (source.toLowerCase()) {
138
166
  case 'youtube':
139
167
  case 'yt':
140
168
  endpoint = `/youtube/stream/${id}`;
@@ -147,6 +175,13 @@ class Streamify extends EventEmitter {
147
175
  case 'sc':
148
176
  endpoint = `/soundcloud/stream/${id}`;
149
177
  break;
178
+ case 'twitch':
179
+ case 'mixcloud':
180
+ case 'bandcamp':
181
+ case 'http':
182
+ case 'local':
183
+ endpoint = `/stream/${source.toLowerCase()}/${encodeURIComponent(id)}`;
184
+ break;
150
185
  default:
151
186
  throw new Error(`Unknown source: ${source}`);
152
187
  }
@@ -191,6 +226,31 @@ class Streamify extends EventEmitter {
191
226
  getStream: (id, filters = {}) => this.getStream('soundcloud', id, filters)
192
227
  };
193
228
 
229
+ twitch = {
230
+ getInfo: (id) => this.getInfo('twitch', id),
231
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('twitch', id, filters)
232
+ };
233
+
234
+ mixcloud = {
235
+ getInfo: (id) => this.getInfo('mixcloud', id),
236
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('mixcloud', id, filters)
237
+ };
238
+
239
+ bandcamp = {
240
+ getInfo: (id) => this.getInfo('bandcamp', id),
241
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('bandcamp', id, filters)
242
+ };
243
+
244
+ http = {
245
+ getInfo: (url) => this.getInfo('http', url),
246
+ getStreamUrl: (url, filters = {}) => this.getStreamUrl('http', url, filters)
247
+ };
248
+
249
+ local = {
250
+ getInfo: (path) => this.getInfo('local', path),
251
+ getStreamUrl: (path, filters = {}) => this.getStreamUrl('local', path, filters)
252
+ };
253
+
194
254
  async getActiveStreams() {
195
255
  const res = await fetch(`${this.getBaseUrl()}/streams`);
196
256
  return res.json();
@@ -249,4 +309,8 @@ Streamify.Manager = Manager;
249
309
  Streamify.Player = Manager ? require('./src/discord/Player') : null;
250
310
  Streamify.Queue = Manager ? require('./src/discord/Queue') : null;
251
311
 
312
+ const { getEffectPresetsInfo, EFFECT_PRESETS } = require('./src/filters/ffmpeg');
313
+ Streamify.getEffectPresetsInfo = getEffectPresetsInfo;
314
+ Streamify.EFFECT_PRESETS = EFFECT_PRESETS;
315
+
252
316
  module.exports = Streamify;
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "streamify-audio",
3
- "version": "2.2.2",
3
+ "version": "2.2.6",
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",
7
7
  "bin": {
8
- "streamify": "./bin/streamify.js"
8
+ "streamify": "bin/streamify.js"
9
9
  },
10
10
  "scripts": {
11
- "start": "node index.js",
12
- "dev": "node index.js --dev"
11
+ "start": "UV_THREADPOOL_SIZE=64 node --noconcurrent-sweeping --max-old-space-size=2048 --expose-gc index.js",
12
+ "dev": "UV_THREADPOOL_SIZE=64 node --noconcurrent-sweeping --max-old-space-size=2048 --expose-gc index.js --dev",
13
+ "test": "node --test tests/*.test.js"
13
14
  },
14
15
  "keywords": [
15
16
  "discord",
@@ -29,7 +30,8 @@
29
30
  "author": "Lucas",
30
31
  "license": "MIT",
31
32
  "dependencies": {
32
- "express": "^4.18.2"
33
+ "express": "^4.18.2",
34
+ "opusscript": "^0.0.8"
33
35
  },
34
36
  "optionalDependencies": {
35
37
  "@discordjs/opus": "^0.9.0",
@@ -48,7 +50,7 @@
48
50
  },
49
51
  "repository": {
50
52
  "type": "git",
51
- "url": "https://github.com/LucasCzechia/streamify.git"
53
+ "url": "git+https://github.com/LucasCzechia/streamify.git"
52
54
  },
53
55
  "homepage": "https://github.com/LucasCzechia/streamify#readme",
54
56
  "bugs": {
@@ -56,6 +56,7 @@ function cleanup() {
56
56
  }
57
57
  }
58
58
 
59
- setInterval(cleanup, 60000);
59
+ const cleanupInterval = setInterval(cleanup, 60000);
60
+ cleanupInterval.unref();
60
61
 
61
62
  module.exports = { get, set, del, clear, stats };
package/src/config.js CHANGED
@@ -18,9 +18,20 @@ const defaults = {
18
18
  clientId: null,
19
19
  clientSecret: null
20
20
  },
21
+ voiceChannelStatus: {
22
+ enabled: false,
23
+ template: '🎶 Now Playing: {title} - {artist} | Requested by: {requester}'
24
+ },
21
25
  audio: {
22
26
  bitrate: '128k',
23
- format: 'opus'
27
+ format: 'opus',
28
+ vbr: true,
29
+ compressionLevel: 10,
30
+ application: 'audio'
31
+ },
32
+ ytdlp: {
33
+ format: 'bestaudio/bestaudio[ext=webm]/bestaudio[ext=mp4]/18/22/best',
34
+ additionalArgs: []
24
35
  },
25
36
  cache: {
26
37
  enabled: true,
@@ -57,6 +68,7 @@ function checkYtdlpVersion(ytdlpPath) {
57
68
  }
58
69
 
59
70
  function load(options = {}) {
71
+ options = options || {};
60
72
  let fileConfig = {};
61
73
 
62
74
  if (options.configPath && fs.existsSync(options.configPath)) {
@@ -90,6 +102,11 @@ function load(options = {}) {
90
102
  ...fileConfig.audio,
91
103
  ...options.audio
92
104
  },
105
+ ytdlp: {
106
+ ...defaults.ytdlp,
107
+ ...fileConfig.ytdlp,
108
+ ...options.ytdlp
109
+ },
93
110
  cache: {
94
111
  ...defaults.cache,
95
112
  ...fileConfig.cache,
@@ -3,6 +3,11 @@ const Player = require('./Player');
3
3
  const youtube = require('../providers/youtube');
4
4
  const spotify = require('../providers/spotify');
5
5
  const soundcloud = require('../providers/soundcloud');
6
+ const twitch = require('../providers/twitch');
7
+ const mixcloud = require('../providers/mixcloud');
8
+ const bandcamp = require('../providers/bandcamp');
9
+ const httpProvider = require('../providers/http');
10
+ const localProvider = require('../providers/local');
6
11
  const log = require('../utils/logger');
7
12
  const { loadConfig } = require('../config');
8
13
 
@@ -73,6 +78,11 @@ class Manager extends EventEmitter {
73
78
  maxTracks: options.autoplay?.maxTracks ?? 5
74
79
  };
75
80
 
81
+ this.voiceChannelStatus = {
82
+ enabled: options.voiceChannelStatus?.enabled ?? false,
83
+ template: options.voiceChannelStatus?.template ?? '🎶 Now Playing: {title} - {artist} | Requested by: {requester}'
84
+ };
85
+
76
86
  this._setupVoiceStateListener();
77
87
 
78
88
  log.info('MANAGER', 'Streamify Manager initialized');
@@ -91,6 +101,10 @@ class Manager extends EventEmitter {
91
101
  }
92
102
 
93
103
  if (oldState.id === botId && newState.channelId && oldState.channelId !== newState.channelId) {
104
+ // Clear status of the old channel
105
+ if (oldState.channelId) {
106
+ this.clearVoiceStatus(oldState.channelId);
107
+ }
94
108
  player.voiceChannelId = newState.channelId;
95
109
  player.emit('channelMove', newState.channelId);
96
110
  return;
@@ -103,12 +117,16 @@ class Manager extends EventEmitter {
103
117
  const members = channel.members.filter(m => !m.user.bot);
104
118
  const memberCount = members.size;
105
119
 
106
- if (oldState.channelId === player.voiceChannelId && newState.channelId !== player.voiceChannelId) {
107
- player.emit('userLeave', oldState.member, memberCount);
108
- }
120
+ // Ignore bots for join/leave events
121
+ const transitionedMember = newState.member || oldState.member;
122
+ if (transitionedMember && !transitionedMember.user.bot) {
123
+ if (oldState.channelId === player.voiceChannelId && newState.channelId !== player.voiceChannelId) {
124
+ player.emit('userLeave', transitionedMember, memberCount);
125
+ }
109
126
 
110
- if (newState.channelId === player.voiceChannelId && oldState.channelId !== player.voiceChannelId) {
111
- player.emit('userJoin', newState.member, memberCount);
127
+ if (newState.channelId === player.voiceChannelId && oldState.channelId !== player.voiceChannelId) {
128
+ player.emit('userJoin', transitionedMember, memberCount);
129
+ }
112
130
  }
113
131
 
114
132
  if (memberCount < player.autoPause.minUsers) {
@@ -165,7 +183,8 @@ class Manager extends EventEmitter {
165
183
  maxPreviousTracks: this.maxPreviousTracks,
166
184
  autoLeave: this.autoLeave,
167
185
  autoPause: this.autoPause,
168
- autoplay: this.autoplay
186
+ autoplay: this.autoplay,
187
+ voiceChannelStatus: this.voiceChannelStatus
169
188
  });
170
189
 
171
190
  player.on('destroy', () => {
@@ -184,6 +203,20 @@ class Manager extends EventEmitter {
184
203
  return this.players.get(guildId);
185
204
  }
186
205
 
206
+ async clearVoiceStatus(channelId) {
207
+ if (!channelId) return false;
208
+ try {
209
+ await this.client.rest.put(`/channels/${channelId}/voice-status`, {
210
+ body: { status: "" }
211
+ });
212
+ log.debug('MANAGER', `Cleared voice status for channel ${channelId}`);
213
+ return true;
214
+ } catch (error) {
215
+ log.debug('MANAGER', `Failed to clear voice status for ${channelId}: ${error.message}`);
216
+ return false;
217
+ }
218
+ }
219
+
187
220
  destroy(guildId) {
188
221
  const player = this.players.get(guildId);
189
222
  if (player) {
@@ -217,7 +250,7 @@ class Manager extends EventEmitter {
217
250
  if (!this._isProviderEnabled('youtube')) {
218
251
  throw new Error('YouTube provider is disabled');
219
252
  }
220
- result = await youtube.search(query, limit, this.config);
253
+ result = await youtube.search(query, limit, this.config, options);
221
254
  break;
222
255
 
223
256
  case 'spotify':
@@ -239,6 +272,31 @@ class Manager extends EventEmitter {
239
272
  result = await soundcloud.search(query, limit, this.config);
240
273
  break;
241
274
 
275
+ case 'twitch':
276
+ result = await twitch.getInfo(query, this.config);
277
+ result = { tracks: [result], source: 'twitch' };
278
+ break;
279
+
280
+ case 'mixcloud':
281
+ result = await mixcloud.getInfo(query, this.config);
282
+ result = { tracks: [result], source: 'mixcloud' };
283
+ break;
284
+
285
+ case 'bandcamp':
286
+ result = await bandcamp.getInfo(query, this.config);
287
+ result = { tracks: [result], source: 'bandcamp' };
288
+ break;
289
+
290
+ case 'local':
291
+ result = await localProvider.getInfo(query, this.config);
292
+ result = { tracks: [result], source: 'local' };
293
+ break;
294
+
295
+ case 'http':
296
+ result = await httpProvider.getInfo(query, this.config);
297
+ result = { tracks: [result], source: 'http' };
298
+ break;
299
+
242
300
  default:
243
301
  if (!this._isProviderEnabled('youtube')) {
244
302
  throw new Error('YouTube provider is disabled');
@@ -285,6 +343,21 @@ class Manager extends EventEmitter {
285
343
  }
286
344
  return await soundcloud.getInfo(id, this.config);
287
345
 
346
+ case 'twitch':
347
+ return await twitch.getInfo(id, this.config);
348
+
349
+ case 'mixcloud':
350
+ return await mixcloud.getInfo(id, this.config);
351
+
352
+ case 'bandcamp':
353
+ return await bandcamp.getInfo(id, this.config);
354
+
355
+ case 'local':
356
+ return await localProvider.getInfo(id, this.config);
357
+
358
+ case 'http':
359
+ return await httpProvider.getInfo(id, this.config);
360
+
288
361
  default:
289
362
  if (!this._isProviderEnabled('youtube')) {
290
363
  throw new Error('YouTube provider is disabled');
@@ -351,6 +424,15 @@ class Manager extends EventEmitter {
351
424
  soundcloud: [
352
425
  /soundcloud\.com\/([^\/]+\/[^\/\?]+)/,
353
426
  /api\.soundcloud\.com\/tracks\/(\d+)/
427
+ ],
428
+ twitch: [
429
+ /twitch\.tv\/([a-zA-Z0-9_]+)/
430
+ ],
431
+ mixcloud: [
432
+ /mixcloud\.com\/([^\/]+\/[^\/]+)/
433
+ ],
434
+ bandcamp: [
435
+ /[a-zA-Z0-9-]+\.bandcamp\.com\/(track|album)\/([a-zA-Z0-9-]+)/
354
436
  ]
355
437
  };
356
438
 
@@ -362,10 +444,22 @@ class Manager extends EventEmitter {
362
444
  }
363
445
  }
364
446
 
447
+ if (input.startsWith('/') || input.startsWith('./') || input.startsWith('file://')) {
448
+ return 'local';
449
+ }
450
+
451
+ if (input.startsWith('http') && /\.(mp3|wav|ogg|flac|m4a|aac)(\?.*)?$/i.test(input)) {
452
+ return 'http';
453
+ }
454
+
365
455
  return null;
366
456
  }
367
457
 
368
458
  _extractId(input, source) {
459
+ if (['twitch', 'mixcloud', 'bandcamp', 'local', 'http'].includes(source)) {
460
+ return { source, id: input };
461
+ }
462
+
369
463
  const patterns = {
370
464
  youtube: /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|music\.youtube\.com\/watch\?v=|youtube\.com\/live\/)([a-zA-Z0-9_-]{11})/,
371
465
  youtube_playlist: /youtube\.com\/playlist\?list=([a-zA-Z0-9_-]+)/,