streamify-audio 2.0.4 → 2.1.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.
- package/index.js +10 -0
- package/package.json +1 -1
- package/src/discord/Manager.js +231 -0
- package/src/discord/Player.js +65 -0
- package/src/discord/Stream.js +18 -1
- package/src/filters/presets.js +227 -0
- package/src/persistence/index.js +156 -0
package/index.js
CHANGED
|
@@ -249,4 +249,14 @@ Streamify.Manager = Manager;
|
|
|
249
249
|
Streamify.Player = Manager ? require('./src/discord/Player') : null;
|
|
250
250
|
Streamify.Queue = Manager ? require('./src/discord/Queue') : null;
|
|
251
251
|
|
|
252
|
+
const persistence = require('./src/persistence');
|
|
253
|
+
Streamify.PersistenceAdapter = persistence.PersistenceAdapter;
|
|
254
|
+
Streamify.MemoryAdapter = persistence.MemoryAdapter;
|
|
255
|
+
Streamify.MongoAdapter = persistence.MongoAdapter;
|
|
256
|
+
|
|
257
|
+
const presets = require('./src/filters/presets');
|
|
258
|
+
Streamify.EffectPresets = presets;
|
|
259
|
+
Streamify.getEffectPresetNames = presets.getPresetNames;
|
|
260
|
+
Streamify.getEffectPresetsInfo = presets.getAllPresetsInfo;
|
|
261
|
+
|
|
252
262
|
module.exports = Streamify;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "streamify-audio",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
package/src/discord/Manager.js
CHANGED
|
@@ -5,6 +5,7 @@ const spotify = require('../providers/spotify');
|
|
|
5
5
|
const soundcloud = require('../providers/soundcloud');
|
|
6
6
|
const log = require('../utils/logger');
|
|
7
7
|
const { loadConfig } = require('../config');
|
|
8
|
+
const { MemoryAdapter, MongoAdapter, serializeTrack, deserializeTrack } = require('../persistence');
|
|
8
9
|
|
|
9
10
|
function checkDependencies() {
|
|
10
11
|
const missing = [];
|
|
@@ -73,11 +74,164 @@ class Manager extends EventEmitter {
|
|
|
73
74
|
maxTracks: options.autoplay?.maxTracks ?? 5
|
|
74
75
|
};
|
|
75
76
|
|
|
77
|
+
this.fallback = {
|
|
78
|
+
enabled: options.fallback?.enabled ?? false,
|
|
79
|
+
order: options.fallback?.order || ['youtube', 'soundcloud']
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.persistence = null;
|
|
83
|
+
this._persistenceInterval = null;
|
|
84
|
+
this._persistenceIntervalMs = options.persistence?.interval || 30000;
|
|
85
|
+
|
|
76
86
|
this._setupVoiceStateListener();
|
|
77
87
|
|
|
78
88
|
log.info('MANAGER', 'Streamify Manager initialized');
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
async enablePersistence(options = {}) {
|
|
92
|
+
if (options.adapter) {
|
|
93
|
+
this.persistence = options.adapter;
|
|
94
|
+
} else if (options.type === 'mongodb') {
|
|
95
|
+
this.persistence = new MongoAdapter({
|
|
96
|
+
client: options.client,
|
|
97
|
+
uri: options.uri,
|
|
98
|
+
db: options.db,
|
|
99
|
+
collection: options.collection
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
this.persistence = new MemoryAdapter();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (options.autoSave !== false) {
|
|
106
|
+
this._persistenceInterval = setInterval(() => {
|
|
107
|
+
this._saveAllSessions();
|
|
108
|
+
}, this._persistenceIntervalMs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
log.info('MANAGER', `Persistence enabled (type: ${options.type || 'memory'})`);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async _saveAllSessions() {
|
|
116
|
+
if (!this.persistence) return;
|
|
117
|
+
|
|
118
|
+
for (const [guildId, player] of this.players) {
|
|
119
|
+
if (player.connected) {
|
|
120
|
+
await this.saveSession(guildId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async saveSession(guildId) {
|
|
126
|
+
if (!this.persistence) return false;
|
|
127
|
+
|
|
128
|
+
const player = this.players.get(guildId);
|
|
129
|
+
if (!player || !player.connected) return false;
|
|
130
|
+
|
|
131
|
+
const current = player.queue.current;
|
|
132
|
+
if (!current && player.queue.tracks.length === 0) {
|
|
133
|
+
await this.persistence.delete(guildId);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sessionData = {
|
|
138
|
+
guildId,
|
|
139
|
+
voiceChannelId: player.voiceChannelId,
|
|
140
|
+
textChannelId: player.textChannelId,
|
|
141
|
+
currentTrack: serializeTrack(current),
|
|
142
|
+
positionMs: player.position || 0,
|
|
143
|
+
queue: player.queue.tracks.map(serializeTrack),
|
|
144
|
+
volume: player.volume,
|
|
145
|
+
loop: player.queue.repeatMode || 'off',
|
|
146
|
+
filters: player.filters || {}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await this.persistence.save(guildId, sessionData);
|
|
150
|
+
log.debug('MANAGER', `Session saved for guild ${guildId}`);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async restoreSession(guildId, options = {}) {
|
|
155
|
+
if (!this.persistence) return null;
|
|
156
|
+
|
|
157
|
+
const session = await this.persistence.load(guildId);
|
|
158
|
+
if (!session) return null;
|
|
159
|
+
|
|
160
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
161
|
+
if (!guild) {
|
|
162
|
+
log.debug('MANAGER', `Guild ${guildId} not found, skipping restore`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const voiceChannel = this.client.channels.cache.get(session.voiceChannelId);
|
|
167
|
+
if (!voiceChannel) {
|
|
168
|
+
log.debug('MANAGER', `Voice channel ${session.voiceChannelId} not found, skipping restore`);
|
|
169
|
+
await this.persistence.delete(guildId);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
log.info('MANAGER', `Restoring session for guild ${guildId}`);
|
|
174
|
+
|
|
175
|
+
const player = await this.create(guildId, session.voiceChannelId, session.textChannelId);
|
|
176
|
+
|
|
177
|
+
if (session.volume !== undefined) player.setVolume(session.volume);
|
|
178
|
+
if (session.loop && session.loop !== 'off') player.setLoop(session.loop);
|
|
179
|
+
|
|
180
|
+
if (session.currentTrack) {
|
|
181
|
+
const track = deserializeTrack(session.currentTrack);
|
|
182
|
+
await player.play(track);
|
|
183
|
+
|
|
184
|
+
if (session.positionMs > 0 && !track.isLive && options.seekToPosition !== false) {
|
|
185
|
+
try {
|
|
186
|
+
await player.seek(session.positionMs);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
log.debug('MANAGER', `Could not seek to position: ${e.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (session.queue && session.queue.length > 0) {
|
|
194
|
+
const tracks = session.queue.map(deserializeTrack);
|
|
195
|
+
player.queue.addMany(tracks);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const members = voiceChannel.members?.filter(m => !m.user.bot);
|
|
199
|
+
if (members && members.size === 0 && player.autoPause.enabled) {
|
|
200
|
+
player.pause();
|
|
201
|
+
player._autoPaused = true;
|
|
202
|
+
log.info('MANAGER', `Session restored but auto-paused (empty channel)`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.emit('sessionRestored', { guildId, player, session });
|
|
206
|
+
return player;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async restoreAllSessions(options = {}) {
|
|
210
|
+
if (!this.persistence) return [];
|
|
211
|
+
|
|
212
|
+
const sessions = await this.persistence.loadAll();
|
|
213
|
+
const restored = [];
|
|
214
|
+
|
|
215
|
+
for (const session of sessions) {
|
|
216
|
+
try {
|
|
217
|
+
const player = await this.restoreSession(session.guildId, options);
|
|
218
|
+
if (player) {
|
|
219
|
+
restored.push({ guildId: session.guildId, player });
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
log.error('MANAGER', `Failed to restore session ${session.guildId}: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
log.info('MANAGER', `Restored ${restored.length}/${sessions.length} sessions`);
|
|
227
|
+
return restored;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async deleteSession(guildId) {
|
|
231
|
+
if (!this.persistence) return false;
|
|
232
|
+
return this.persistence.delete(guildId);
|
|
233
|
+
}
|
|
234
|
+
|
|
81
235
|
_setupVoiceStateListener() {
|
|
82
236
|
this.client.on('voiceStateUpdate', (oldState, newState) => {
|
|
83
237
|
const player = this.players.get(oldState.guild.id) || this.players.get(newState.guild.id);
|
|
@@ -474,7 +628,84 @@ class Manager extends EventEmitter {
|
|
|
474
628
|
}
|
|
475
629
|
}
|
|
476
630
|
|
|
631
|
+
async searchWithFallback(query, options = {}) {
|
|
632
|
+
if (!this.fallback.enabled) {
|
|
633
|
+
return this.search(query, options);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const sources = options.sources || this.fallback.order;
|
|
637
|
+
let lastError = null;
|
|
638
|
+
|
|
639
|
+
for (const source of sources) {
|
|
640
|
+
try {
|
|
641
|
+
const result = await this.search(query, { ...options, source });
|
|
642
|
+
if (result.loadType !== 'error' && result.tracks.length > 0) {
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
} catch (error) {
|
|
646
|
+
lastError = error;
|
|
647
|
+
log.debug('MANAGER', `Fallback: ${source} failed, trying next...`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
loadType: 'error',
|
|
653
|
+
tracks: [],
|
|
654
|
+
error: lastError?.message || 'All sources failed'
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async resolveWithFallback(query) {
|
|
659
|
+
if (!this.fallback.enabled) {
|
|
660
|
+
return this.resolve(query);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const result = await this.resolve(query);
|
|
664
|
+
if (result.loadType !== 'error' && result.tracks.length > 0) {
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const detected = this._detectSource(query);
|
|
669
|
+
if (detected === 'spotify') {
|
|
670
|
+
const match = this._extractId(query, detected);
|
|
671
|
+
if (match) {
|
|
672
|
+
try {
|
|
673
|
+
const track = await this.getInfo(match.id, 'spotify');
|
|
674
|
+
const searchQuery = `${track.author} - ${track.title}`;
|
|
675
|
+
|
|
676
|
+
for (const source of this.fallback.order) {
|
|
677
|
+
if (source === 'spotify') continue;
|
|
678
|
+
try {
|
|
679
|
+
const fallbackResult = await this.search(searchQuery, { source, limit: 1 });
|
|
680
|
+
if (fallbackResult.tracks.length > 0) {
|
|
681
|
+
const fallbackTrack = fallbackResult.tracks[0];
|
|
682
|
+
fallbackTrack._originalSpotifyTrack = track;
|
|
683
|
+
log.info('MANAGER', `Spotify fallback: resolved via ${source}`);
|
|
684
|
+
return {
|
|
685
|
+
loadType: 'track',
|
|
686
|
+
tracks: [fallbackTrack],
|
|
687
|
+
fallbackUsed: source
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
} catch (e) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
log.debug('MANAGER', `Spotify info fetch failed: ${e.message}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
|
|
477
703
|
destroyAll() {
|
|
704
|
+
if (this._persistenceInterval) {
|
|
705
|
+
clearInterval(this._persistenceInterval);
|
|
706
|
+
this._persistenceInterval = null;
|
|
707
|
+
}
|
|
708
|
+
|
|
478
709
|
for (const [guildId, player] of this.players) {
|
|
479
710
|
player.destroy();
|
|
480
711
|
}
|
package/src/discord/Player.js
CHANGED
|
@@ -2,6 +2,7 @@ const { EventEmitter } = require('events');
|
|
|
2
2
|
const Queue = require('./Queue');
|
|
3
3
|
const { createStream } = require('./Stream');
|
|
4
4
|
const log = require('../utils/logger');
|
|
5
|
+
const { buildPresetFilters, getPresetNames, getAllPresetsInfo } = require('../filters/presets');
|
|
5
6
|
|
|
6
7
|
let voiceModule;
|
|
7
8
|
try {
|
|
@@ -587,6 +588,70 @@ class Player extends EventEmitter {
|
|
|
587
588
|
return this.setFilter('preset', presetName);
|
|
588
589
|
}
|
|
589
590
|
|
|
591
|
+
async setEffectPreset(presetName, intensity = 0.5) {
|
|
592
|
+
const filters = buildPresetFilters(presetName, intensity);
|
|
593
|
+
|
|
594
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
595
|
+
if (key === '_intensityFactor') continue;
|
|
596
|
+
this._filters[key] = value;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
this._activePresets = this._activePresets || new Map();
|
|
600
|
+
this._activePresets.set(presetName, intensity);
|
|
601
|
+
|
|
602
|
+
if (this._playing && this.queue.current) {
|
|
603
|
+
return this.setFilter('_trigger', null);
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async setEffectPresets(presets) {
|
|
609
|
+
this._activePresets = new Map();
|
|
610
|
+
|
|
611
|
+
for (const { name, intensity = 0.5 } of presets) {
|
|
612
|
+
const filters = buildPresetFilters(name, intensity);
|
|
613
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
614
|
+
if (key === '_intensityFactor') continue;
|
|
615
|
+
this._filters[key] = value;
|
|
616
|
+
}
|
|
617
|
+
this._activePresets.set(name, intensity);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (this._playing && this.queue.current) {
|
|
621
|
+
return this.setFilter('_trigger', null);
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async clearEffectPresets() {
|
|
627
|
+
const presetsToRemove = ['bass', 'treble', 'speed', 'pitch', 'rotation', 'karaoke',
|
|
628
|
+
'lowpass', 'highpass', 'bandpass', 'vibrato', 'tremolo', 'chorus',
|
|
629
|
+
'compressor', 'normalizer', 'nightcore', 'vaporwave', 'bassboost', '8d'];
|
|
630
|
+
|
|
631
|
+
for (const key of presetsToRemove) {
|
|
632
|
+
delete this._filters[key];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
this._activePresets = new Map();
|
|
636
|
+
|
|
637
|
+
if (this._playing && this.queue.current) {
|
|
638
|
+
return this.setFilter('_trigger', null);
|
|
639
|
+
}
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
getActiveEffectPresets() {
|
|
644
|
+
return this._activePresets ? Array.from(this._activePresets.entries()).map(([name, intensity]) => ({ name, intensity })) : [];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
getAvailableEffectPresets() {
|
|
648
|
+
return getAllPresetsInfo();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
static getEffectPresetNames() {
|
|
652
|
+
return getPresetNames();
|
|
653
|
+
}
|
|
654
|
+
|
|
590
655
|
async clearEQ() {
|
|
591
656
|
delete this._filters.equalizer;
|
|
592
657
|
delete this._filters.preset;
|
package/src/discord/Stream.js
CHANGED
|
@@ -34,9 +34,26 @@ class StreamController {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
this.startTime = Date.now();
|
|
37
|
-
const videoId = this.track._resolvedId || this.track.id;
|
|
38
37
|
const source = this.track.source || 'youtube';
|
|
39
38
|
|
|
39
|
+
let videoId = this.track._resolvedId || this.track.id;
|
|
40
|
+
|
|
41
|
+
if (source === 'spotify' && !this.track._resolvedId) {
|
|
42
|
+
log.info('STREAM', `Resolving Spotify track to YouTube: ${this.track.title}`);
|
|
43
|
+
try {
|
|
44
|
+
const spotify = require('../providers/spotify');
|
|
45
|
+
videoId = await spotify.resolveToYouTube(this.track.id, this.config);
|
|
46
|
+
this.track._resolvedId = videoId;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
log.error('STREAM', `Spotify resolution failed: ${error.message}`);
|
|
49
|
+
throw new Error(`Failed to resolve Spotify track: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!videoId || videoId === 'undefined') {
|
|
54
|
+
throw new Error(`Invalid track ID: ${videoId} (source: ${source}, title: ${this.track.title})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
log.info('STREAM', `Creating stream for ${videoId} (${source})`);
|
|
41
58
|
|
|
42
59
|
let url;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const EFFECT_PRESETS = {
|
|
2
|
+
bassboost: {
|
|
3
|
+
description: 'Boost bass frequencies',
|
|
4
|
+
build: (intensity = 0.5) => {
|
|
5
|
+
const gain = 5 + (intensity * 15);
|
|
6
|
+
return { bass: gain };
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
nightcore: {
|
|
11
|
+
description: 'Speed up with higher pitch (anime style)',
|
|
12
|
+
build: (intensity = 0.5) => {
|
|
13
|
+
const speedBoost = 0.15 + (intensity * 0.2);
|
|
14
|
+
return {
|
|
15
|
+
speed: 1 + speedBoost,
|
|
16
|
+
pitch: 1 + speedBoost
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
vaporwave: {
|
|
22
|
+
description: 'Slow down with lower pitch (aesthetic)',
|
|
23
|
+
build: (intensity = 0.5) => {
|
|
24
|
+
const slowdown = 0.1 + (intensity * 0.2);
|
|
25
|
+
return {
|
|
26
|
+
speed: 1 - slowdown,
|
|
27
|
+
pitch: 1 - slowdown
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
'8d': {
|
|
33
|
+
description: 'Rotating 8D audio effect',
|
|
34
|
+
build: (intensity = 0.5) => {
|
|
35
|
+
const speed = 0.05 + (intensity * 0.2);
|
|
36
|
+
return { rotation: { speed } };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
karaoke: {
|
|
41
|
+
description: 'Reduce vocals (center channel removal)',
|
|
42
|
+
build: () => ({ karaoke: true })
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
trebleboost: {
|
|
46
|
+
description: 'Boost high frequencies',
|
|
47
|
+
build: (intensity = 0.5) => {
|
|
48
|
+
const gain = 5 + (intensity * 15);
|
|
49
|
+
return { treble: gain };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
deep: {
|
|
54
|
+
description: 'Deep bass with reduced highs',
|
|
55
|
+
build: (intensity = 0.5) => {
|
|
56
|
+
const bassGain = 8 + (intensity * 12);
|
|
57
|
+
const trebleCut = -3 - (intensity * 7);
|
|
58
|
+
return { bass: bassGain, treble: trebleCut };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
pop: {
|
|
63
|
+
description: 'Enhanced clarity for pop music',
|
|
64
|
+
build: (intensity = 0.5) => {
|
|
65
|
+
const factor = 0.5 + (intensity * 0.5);
|
|
66
|
+
return {
|
|
67
|
+
preset: 'pop',
|
|
68
|
+
_intensityFactor: factor
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
rock: {
|
|
74
|
+
description: 'Punchy mids and lows for rock',
|
|
75
|
+
build: (intensity = 0.5) => {
|
|
76
|
+
const factor = 0.5 + (intensity * 0.5);
|
|
77
|
+
return {
|
|
78
|
+
preset: 'rock',
|
|
79
|
+
_intensityFactor: factor
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
electronic: {
|
|
85
|
+
description: 'Enhanced bass and highs for EDM',
|
|
86
|
+
build: (intensity = 0.5) => {
|
|
87
|
+
const factor = 0.5 + (intensity * 0.5);
|
|
88
|
+
return {
|
|
89
|
+
preset: 'electronic',
|
|
90
|
+
_intensityFactor: factor
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
lofi: {
|
|
96
|
+
description: 'Lo-fi aesthetic with warmth',
|
|
97
|
+
build: (intensity = 0.5) => ({
|
|
98
|
+
lowpass: 8000 - (intensity * 4000),
|
|
99
|
+
bass: 3 + (intensity * 5)
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
radio: {
|
|
104
|
+
description: 'AM radio effect',
|
|
105
|
+
build: (intensity = 0.5) => ({
|
|
106
|
+
lowpass: 5000 - (intensity * 2000),
|
|
107
|
+
highpass: 300 + (intensity * 200)
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
telephone: {
|
|
112
|
+
description: 'Telephone/walkie-talkie effect',
|
|
113
|
+
build: () => ({
|
|
114
|
+
bandpass: { frequency: 1500, width: 1000 }
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
soft: {
|
|
119
|
+
description: 'Soft, gentle sound',
|
|
120
|
+
build: (intensity = 0.5) => ({
|
|
121
|
+
lowpass: 12000 - (intensity * 4000),
|
|
122
|
+
compressor: true
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
loud: {
|
|
127
|
+
description: 'Louder, more compressed',
|
|
128
|
+
build: (intensity = 0.5) => ({
|
|
129
|
+
preset: 'loudness',
|
|
130
|
+
compressor: true,
|
|
131
|
+
normalizer: true
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
chipmunk: {
|
|
136
|
+
description: 'High-pitched chipmunk voice',
|
|
137
|
+
build: (intensity = 0.5) => ({
|
|
138
|
+
pitch: 1.3 + (intensity * 0.5),
|
|
139
|
+
speed: 1.2 + (intensity * 0.3)
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
darth: {
|
|
144
|
+
description: 'Deep Darth Vader-like voice',
|
|
145
|
+
build: (intensity = 0.5) => ({
|
|
146
|
+
pitch: 0.7 - (intensity * 0.2),
|
|
147
|
+
speed: 0.9 - (intensity * 0.1)
|
|
148
|
+
})
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
echo: {
|
|
152
|
+
description: 'Echo/reverb effect',
|
|
153
|
+
build: () => ({
|
|
154
|
+
chorus: true
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
vibrato: {
|
|
159
|
+
description: 'Vibrating pitch effect',
|
|
160
|
+
build: (intensity = 0.5) => ({
|
|
161
|
+
vibrato: {
|
|
162
|
+
frequency: 4 + (intensity * 6),
|
|
163
|
+
depth: 0.3 + (intensity * 0.4)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
tremolo: {
|
|
169
|
+
description: 'Trembling volume effect',
|
|
170
|
+
build: (intensity = 0.5) => ({
|
|
171
|
+
tremolo: {
|
|
172
|
+
frequency: 4 + (intensity * 8),
|
|
173
|
+
depth: 0.4 + (intensity * 0.4)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function getPresetNames() {
|
|
180
|
+
return Object.keys(EFFECT_PRESETS);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getPresetInfo(name) {
|
|
184
|
+
const preset = EFFECT_PRESETS[name];
|
|
185
|
+
if (!preset) return null;
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
description: preset.description
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getAllPresetsInfo() {
|
|
193
|
+
return Object.entries(EFFECT_PRESETS).map(([name, preset]) => ({
|
|
194
|
+
name,
|
|
195
|
+
description: preset.description
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildPresetFilters(name, intensity = 0.5) {
|
|
200
|
+
const preset = EFFECT_PRESETS[name];
|
|
201
|
+
if (!preset) {
|
|
202
|
+
throw new Error(`Unknown preset: ${name}. Available: ${getPresetNames().join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const clampedIntensity = Math.max(0, Math.min(1, intensity));
|
|
206
|
+
return preset.build(clampedIntensity);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function combinePresets(presetNames, intensity = 0.5) {
|
|
210
|
+
const combined = {};
|
|
211
|
+
|
|
212
|
+
for (const name of presetNames) {
|
|
213
|
+
const filters = buildPresetFilters(name, intensity);
|
|
214
|
+
Object.assign(combined, filters);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return combined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
EFFECT_PRESETS,
|
|
222
|
+
getPresetNames,
|
|
223
|
+
getPresetInfo,
|
|
224
|
+
getAllPresetsInfo,
|
|
225
|
+
buildPresetFilters,
|
|
226
|
+
combinePresets
|
|
227
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const log = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
class PersistenceAdapter {
|
|
4
|
+
async save(guildId, sessionData) {
|
|
5
|
+
throw new Error('save() must be implemented by adapter');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async load(guildId) {
|
|
9
|
+
throw new Error('load() must be implemented by adapter');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async delete(guildId) {
|
|
13
|
+
throw new Error('delete() must be implemented by adapter');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async loadAll() {
|
|
17
|
+
throw new Error('loadAll() must be implemented by adapter');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class MemoryAdapter extends PersistenceAdapter {
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.sessions = new Map();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async save(guildId, sessionData) {
|
|
28
|
+
this.sessions.set(guildId, {
|
|
29
|
+
...sessionData,
|
|
30
|
+
updatedAt: Date.now()
|
|
31
|
+
});
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async load(guildId) {
|
|
36
|
+
return this.sessions.get(guildId) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async delete(guildId) {
|
|
40
|
+
return this.sessions.delete(guildId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async loadAll() {
|
|
44
|
+
return Array.from(this.sessions.entries()).map(([guildId, data]) => ({
|
|
45
|
+
guildId,
|
|
46
|
+
...data
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class MongoAdapter extends PersistenceAdapter {
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
super();
|
|
54
|
+
this.collection = null;
|
|
55
|
+
this.collectionName = options.collection || 'streamify_sessions';
|
|
56
|
+
this.mongoClient = options.client || null;
|
|
57
|
+
this.mongoUri = options.uri || null;
|
|
58
|
+
this.db = options.db || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async connect() {
|
|
62
|
+
if (this.collection) return;
|
|
63
|
+
|
|
64
|
+
if (this.mongoClient) {
|
|
65
|
+
const dbName = this.db || 'streamify';
|
|
66
|
+
this.collection = this.mongoClient.db(dbName).collection(this.collectionName);
|
|
67
|
+
} else if (this.mongoUri) {
|
|
68
|
+
const { MongoClient } = require('mongodb');
|
|
69
|
+
const client = new MongoClient(this.mongoUri);
|
|
70
|
+
await client.connect();
|
|
71
|
+
const dbName = this.db || 'streamify';
|
|
72
|
+
this.collection = client.db(dbName).collection(this.collectionName);
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error('MongoAdapter requires either client or uri option');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log.info('PERSISTENCE', `Connected to MongoDB collection: ${this.collectionName}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async save(guildId, sessionData) {
|
|
81
|
+
await this.connect();
|
|
82
|
+
await this.collection.updateOne(
|
|
83
|
+
{ _id: guildId },
|
|
84
|
+
{ $set: { ...sessionData, updatedAt: new Date() } },
|
|
85
|
+
{ upsert: true }
|
|
86
|
+
);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async load(guildId) {
|
|
91
|
+
await this.connect();
|
|
92
|
+
const doc = await this.collection.findOne({ _id: guildId });
|
|
93
|
+
return doc || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async delete(guildId) {
|
|
97
|
+
await this.connect();
|
|
98
|
+
const result = await this.collection.deleteOne({ _id: guildId });
|
|
99
|
+
return result.deletedCount > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async loadAll() {
|
|
103
|
+
await this.connect();
|
|
104
|
+
const docs = await this.collection.find({}).toArray();
|
|
105
|
+
return docs.map(doc => ({
|
|
106
|
+
guildId: doc._id,
|
|
107
|
+
...doc
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function serializeTrack(track) {
|
|
113
|
+
if (!track) return null;
|
|
114
|
+
return {
|
|
115
|
+
id: track.id,
|
|
116
|
+
title: track.title,
|
|
117
|
+
author: track.author,
|
|
118
|
+
duration: track.duration,
|
|
119
|
+
thumbnail: track.thumbnail,
|
|
120
|
+
uri: track.uri,
|
|
121
|
+
source: track.source,
|
|
122
|
+
isLive: track.isLive || false,
|
|
123
|
+
_resolvedId: track._resolvedId,
|
|
124
|
+
requestedBy: track.requestedBy ? {
|
|
125
|
+
id: track.requestedBy.id || null,
|
|
126
|
+
username: track.requestedBy.username || track.requestedBy,
|
|
127
|
+
avatarUrl: track.requestedBy.avatarUrl || null
|
|
128
|
+
} : null,
|
|
129
|
+
artistUrl: track.artistUrl || null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function deserializeTrack(data) {
|
|
134
|
+
if (!data) return null;
|
|
135
|
+
return {
|
|
136
|
+
id: data.id,
|
|
137
|
+
title: data.title,
|
|
138
|
+
author: data.author,
|
|
139
|
+
duration: data.duration,
|
|
140
|
+
thumbnail: data.thumbnail,
|
|
141
|
+
uri: data.uri,
|
|
142
|
+
source: data.source,
|
|
143
|
+
isLive: data.isLive || false,
|
|
144
|
+
_resolvedId: data._resolvedId,
|
|
145
|
+
requestedBy: data.requestedBy,
|
|
146
|
+
artistUrl: data.artistUrl
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
PersistenceAdapter,
|
|
152
|
+
MemoryAdapter,
|
|
153
|
+
MongoAdapter,
|
|
154
|
+
serializeTrack,
|
|
155
|
+
deserializeTrack
|
|
156
|
+
};
|