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.
- package/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +81 -7
- package/docs/configuration.md +31 -2
- package/docs/discord/manager.md +14 -7
- package/docs/discord/player.md +84 -1
- package/docs/filters.md +39 -1
- package/docs/http/endpoints.md +25 -0
- package/docs/http/server.md +2 -0
- package/docs/sources.md +52 -4
- package/index.d.ts +31 -1
- package/index.js +67 -3
- package/package.json +8 -6
- package/src/cache/index.js +2 -1
- package/src/config.js +18 -1
- package/src/discord/Manager.js +101 -7
- package/src/discord/Player.js +281 -99
- package/src/discord/Stream.js +204 -137
- package/src/filters/ffmpeg.js +124 -15
- package/src/providers/bandcamp.js +49 -0
- package/src/providers/http.js +35 -0
- package/src/providers/local.js +36 -0
- package/src/providers/mixcloud.js +49 -0
- package/src/providers/soundcloud.js +5 -1
- package/src/providers/twitch.js +49 -0
- package/src/providers/youtube.js +58 -17
- package/src/server.js +60 -5
- package/src/utils/logger.js +34 -12
- package/tests/cache.test.js +234 -0
- package/tests/config.test.js +44 -0
- package/tests/error-handling.test.js +318 -0
- package/tests/ffmpeg.test.js +66 -0
- package/tests/filters-edge.test.js +333 -0
- package/tests/http.test.js +24 -0
- package/tests/integration.test.js +325 -0
- package/tests/local.test.js +37 -0
- package/tests/queue.test.js +94 -0
- package/tests/spotify.test.js +238 -0
- package/tests/stream.test.js +217 -0
- package/tests/twitch.test.js +42 -0
- package/tests/utils.test.js +60 -0
- 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.
|
|
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": "
|
|
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": {
|
package/src/cache/index.js
CHANGED
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,
|
package/src/discord/Manager.js
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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_-]+)/,
|