streamify-audio 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,350 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Client, GuildMember } from 'discord.js';
3
+
4
+ declare module 'streamify-audio' {
5
+ // ========================================================================
6
+ // Common Types
7
+ // ========================================================================
8
+
9
+ export interface Track {
10
+ id: string;
11
+ title: string;
12
+ author: string;
13
+ duration: number;
14
+ thumbnail?: string;
15
+ uri: string;
16
+ streamUrl: string;
17
+ source: 'youtube' | 'spotify' | 'soundcloud';
18
+ album?: string;
19
+ isAutoplay?: boolean;
20
+ requestedBy?: any;
21
+ _resolvedId?: string;
22
+ }
23
+
24
+ export interface SearchResult {
25
+ loadType: 'search' | 'track' | 'empty' | 'error';
26
+ tracks: Track[];
27
+ source: string;
28
+ searchTime?: number;
29
+ }
30
+
31
+ export interface PlaylistResult {
32
+ loadType: 'playlist' | 'error';
33
+ playlist?: {
34
+ id: string;
35
+ title: string;
36
+ author: string;
37
+ thumbnail?: string;
38
+ source: string;
39
+ };
40
+ tracks: Track[];
41
+ error?: string;
42
+ }
43
+
44
+ export interface Filters {
45
+ bass?: number;
46
+ treble?: number;
47
+ speed?: number;
48
+ pitch?: number;
49
+ volume?: number;
50
+ equalizer?: number[];
51
+ preset?: string;
52
+ tremolo?: { frequency?: number; depth?: number };
53
+ vibrato?: { frequency?: number; depth?: number };
54
+ rotation?: { speed?: number };
55
+ lowpass?: number;
56
+ highpass?: number;
57
+ bandpass?: { frequency?: number; width?: number };
58
+ bandreject?: { frequency?: number; width?: number };
59
+ lowshelf?: { frequency?: number; gain?: number };
60
+ highshelf?: { frequency?: number; gain?: number };
61
+ peaking?: { frequency?: number; gain?: number; q?: number };
62
+ karaoke?: boolean;
63
+ mono?: boolean;
64
+ surround?: boolean;
65
+ flanger?: boolean;
66
+ phaser?: boolean;
67
+ chorus?: boolean;
68
+ compressor?: boolean;
69
+ normalizer?: boolean;
70
+ nightcore?: boolean;
71
+ vaporwave?: boolean;
72
+ bassboost?: boolean;
73
+ '8d'?: boolean;
74
+ start?: number;
75
+ }
76
+
77
+ // ========================================================================
78
+ // HTTP Server Mode
79
+ // ========================================================================
80
+
81
+ export interface ProviderConfig {
82
+ enabled?: boolean;
83
+ }
84
+
85
+ export interface ProvidersConfig {
86
+ youtube?: ProviderConfig;
87
+ spotify?: ProviderConfig;
88
+ soundcloud?: ProviderConfig;
89
+ }
90
+
91
+ export interface StreamifyOptions {
92
+ port?: number;
93
+ host?: string;
94
+ cookiesPath?: string;
95
+ cookies?: string;
96
+ ytdlpPath?: string;
97
+ ffmpegPath?: string;
98
+ providers?: ProvidersConfig;
99
+ spotify?: {
100
+ clientId: string;
101
+ clientSecret: string;
102
+ };
103
+ audio?: {
104
+ bitrate?: string;
105
+ format?: 'opus' | 'mp3' | 'aac';
106
+ };
107
+ logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
108
+ silent?: boolean;
109
+ colors?: boolean;
110
+ }
111
+
112
+ export interface ActiveStream {
113
+ id: string;
114
+ source: string;
115
+ trackId: string;
116
+ position: number;
117
+ filters: Filters;
118
+ startTime: number;
119
+ duration: number;
120
+ }
121
+
122
+ export interface SourceMethods {
123
+ search(query: string, limit?: number): Promise<SearchResult>;
124
+ getInfo?(id: string): Promise<Track>;
125
+ getStreamUrl(id: string, filters?: Filters): string;
126
+ getStream(id: string, filters?: Filters): Promise<ReadableStream>;
127
+ }
128
+
129
+ export default class Streamify extends EventEmitter {
130
+ constructor(options?: StreamifyOptions);
131
+
132
+ readonly running: boolean;
133
+ readonly config: StreamifyOptions;
134
+
135
+ start(): Promise<this>;
136
+ stop(): Promise<void>;
137
+ getBaseUrl(): string;
138
+
139
+ detectSource(input: string): { source: string | null; id: string | null; isUrl: boolean };
140
+ resolve(input: string, limit?: number): Promise<SearchResult>;
141
+ search(source: string, query: string, limit?: number): Promise<SearchResult>;
142
+ getInfo(source: string, id: string): Promise<Track>;
143
+ getStreamUrl(source: string, id: string, filters?: Filters): string;
144
+ getStream(source: string, id: string, filters?: Filters): Promise<ReadableStream>;
145
+
146
+ getActiveStreams(): Promise<{ streams: ActiveStream[] }>;
147
+ getStreamInfo(streamId: string): Promise<ActiveStream | null>;
148
+ getPosition(streamId: string): Promise<number | null>;
149
+ applyFilters(source: string, trackId: string, currentStreamId: string, newFilters: Filters): Promise<{
150
+ url: string;
151
+ position: number;
152
+ filters: Filters;
153
+ }>;
154
+
155
+ youtube: SourceMethods;
156
+ spotify: SourceMethods;
157
+ soundcloud: Omit<SourceMethods, 'getInfo'>;
158
+
159
+ on(event: 'streamStart', listener: (data: { id: string; source: string; trackId: string; filters: Filters }) => void): this;
160
+ on(event: 'streamEnd', listener: (data: { id: string; source: string; trackId: string; duration: number }) => void): this;
161
+ on(event: 'streamError', listener: (data: { id: string; source: string; trackId: string; error: Error }) => void): this;
162
+
163
+ static Manager: typeof Manager;
164
+ static Player: typeof Player;
165
+ static Queue: typeof Queue;
166
+ }
167
+
168
+ // ========================================================================
169
+ // Discord Player Mode
170
+ // ========================================================================
171
+
172
+ export interface ManagerOptions {
173
+ ytdlpPath?: string;
174
+ ffmpegPath?: string;
175
+ cookiesPath?: string;
176
+ providers?: ProvidersConfig;
177
+ spotify?: {
178
+ clientId: string;
179
+ clientSecret: string;
180
+ };
181
+ audio?: {
182
+ bitrate?: string;
183
+ format?: 'opus' | 'mp3' | 'aac';
184
+ };
185
+ defaultVolume?: number;
186
+ maxPreviousTracks?: number;
187
+ sponsorblock?: {
188
+ enabled?: boolean;
189
+ categories?: string[];
190
+ };
191
+ autoLeave?: {
192
+ enabled?: boolean;
193
+ emptyDelay?: number;
194
+ inactivityTimeout?: number;
195
+ };
196
+ autoPause?: {
197
+ enabled?: boolean;
198
+ minUsers?: number;
199
+ };
200
+ autoplay?: {
201
+ enabled?: boolean;
202
+ maxTracks?: number;
203
+ };
204
+ }
205
+
206
+ export interface ManagerStats {
207
+ players: number;
208
+ playingPlayers: number;
209
+ memory: {
210
+ heapUsed: number;
211
+ heapTotal: number;
212
+ rss: number;
213
+ };
214
+ }
215
+
216
+ export class Manager extends EventEmitter {
217
+ constructor(client: Client, options?: ManagerOptions);
218
+
219
+ players: Map<string, Player>;
220
+ config: any;
221
+ autoLeave: { enabled: boolean; emptyDelay: number; inactivityTimeout: number };
222
+ autoPause: { enabled: boolean; minUsers: number };
223
+ autoplay: { enabled: boolean; maxTracks: number };
224
+
225
+ create(guildId: string, voiceChannelId: string, textChannelId?: string): Promise<Player>;
226
+ get(guildId: string): Player | undefined;
227
+ destroy(guildId: string): void;
228
+ destroyAll(): void;
229
+
230
+ search(query: string, options?: { source?: string; limit?: number }): Promise<SearchResult>;
231
+ resolve(input: string): Promise<SearchResult>;
232
+ loadPlaylist(url: string): Promise<PlaylistResult>;
233
+ getRelated(track: Track, limit?: number): Promise<{ tracks: Track[]; source: string }>;
234
+ getStats(): ManagerStats;
235
+
236
+ on(event: 'playerCreate', listener: (player: Player) => void): this;
237
+ on(event: 'playerDestroy', listener: (player: Player) => void): this;
238
+ }
239
+
240
+ export class Player extends EventEmitter {
241
+ constructor(manager: Manager, options: any);
242
+
243
+ manager: Manager;
244
+ guildId: string;
245
+ voiceChannelId: string;
246
+ textChannelId: string;
247
+ queue: Queue;
248
+
249
+ readonly connected: boolean;
250
+ readonly playing: boolean;
251
+ readonly paused: boolean;
252
+ readonly position: number;
253
+ volume: number;
254
+ readonly filters: Filters;
255
+
256
+ autoplay: { enabled: boolean; maxTracks: number };
257
+ autoPause: { enabled: boolean; minUsers: number };
258
+ autoLeave: { enabled: boolean; emptyDelay: number; inactivityTimeout: number };
259
+
260
+ connect(): Promise<void>;
261
+ disconnect(): boolean;
262
+ destroy(): void;
263
+
264
+ play(track: Track): Promise<void>;
265
+ pause(destroyStream?: boolean): boolean;
266
+ resume(): Promise<boolean>;
267
+ skip(): Promise<Track | null>;
268
+ previous(): Promise<Track | null>;
269
+ stop(): boolean;
270
+ seek(positionMs: number): Promise<boolean>;
271
+
272
+ setVolume(volume: number): number;
273
+ setLoop(mode: 'off' | 'track' | 'queue'): string;
274
+ setAutoplay(enabled: boolean): boolean;
275
+ setAutoPause(enabled: boolean): boolean;
276
+
277
+ setFilter(name: string, value: any): Promise<boolean>;
278
+ clearFilters(): Promise<boolean>;
279
+ setEQ(bands: number[]): Promise<boolean>;
280
+ setPreset(name: string): Promise<boolean>;
281
+ clearEQ(): Promise<boolean>;
282
+ getPresets(): string[];
283
+
284
+ toJSON(): any;
285
+
286
+ on(event: 'trackStart', listener: (track: Track) => void): this;
287
+ on(event: 'trackEnd', listener: (track: Track, reason: 'finished' | 'skipped' | 'stopped') => void): this;
288
+ on(event: 'trackError', listener: (track: Track, error: Error) => void): this;
289
+ on(event: 'queueEnd', listener: () => void): this;
290
+ on(event: 'userJoin', listener: (member: GuildMember, count: number) => void): this;
291
+ on(event: 'userLeave', listener: (member: GuildMember, count: number) => void): this;
292
+ on(event: 'channelEmpty', listener: () => void): this;
293
+ on(event: 'channelMove', listener: (channelId: string) => void): this;
294
+ on(event: 'autoPause', listener: (userCount: number) => void): this;
295
+ on(event: 'autoResume', listener: (userCount: number) => void): this;
296
+ on(event: 'autoplayStart', listener: (lastTrack: Track) => void): this;
297
+ on(event: 'autoplayAdd', listener: (tracks: Track[]) => void): this;
298
+ on(event: 'destroy', listener: () => void): this;
299
+ }
300
+
301
+ export class Queue {
302
+ constructor(options?: { maxPreviousTracks?: number });
303
+
304
+ current: Track | null;
305
+ tracks: Track[];
306
+ previous: Track[];
307
+ repeatMode: 'off' | 'track' | 'queue';
308
+
309
+ readonly size: number;
310
+ readonly isEmpty: boolean;
311
+ readonly totalDuration: number;
312
+
313
+ add(track: Track, position?: number): void;
314
+ addMany(tracks: Track[]): void;
315
+ remove(index: number): Track | null;
316
+ clear(): void;
317
+ shuffle(): void;
318
+ move(from: number, to: number): boolean;
319
+
320
+ shift(): Track | null;
321
+ setCurrent(track: Track | null): void;
322
+ setRepeatMode(mode: 'off' | 'track' | 'queue'): string;
323
+
324
+ toJSON(): any;
325
+ }
326
+
327
+ // ========================================================================
328
+ // Filter Presets
329
+ // ========================================================================
330
+
331
+ export const PRESETS: {
332
+ flat: number[];
333
+ rock: number[];
334
+ pop: number[];
335
+ jazz: number[];
336
+ classical: number[];
337
+ electronic: number[];
338
+ hiphop: number[];
339
+ acoustic: number[];
340
+ rnb: number[];
341
+ latin: number[];
342
+ loudness: number[];
343
+ piano: number[];
344
+ vocal: number[];
345
+ bass_heavy: number[];
346
+ treble_heavy: number[];
347
+ };
348
+
349
+ export const EQ_BANDS: number[];
350
+ }
package/index.js ADDED
@@ -0,0 +1,252 @@
1
+ const { EventEmitter } = require('events');
2
+ const Server = require('./src/server');
3
+ const config = require('./src/config');
4
+ const youtube = require('./src/providers/youtube');
5
+ const spotify = require('./src/providers/spotify');
6
+ const soundcloud = require('./src/providers/soundcloud');
7
+ const log = require('./src/utils/logger');
8
+ const { setEventEmitter } = require('./src/utils/stream');
9
+
10
+ class Streamify extends EventEmitter {
11
+ constructor(options = {}) {
12
+ super();
13
+ this.options = options;
14
+ this.config = null;
15
+ this.server = null;
16
+ this.running = false;
17
+ }
18
+
19
+ async start() {
20
+ if (this.running) return this;
21
+
22
+ this.config = config.load(this.options);
23
+ log.init(this.config);
24
+ setEventEmitter(this);
25
+ this.server = new Server(this.config);
26
+ await this.server.start();
27
+ this.running = true;
28
+ return this;
29
+ }
30
+
31
+ async stop() {
32
+ if (this.server) {
33
+ await this.server.stop();
34
+ this.running = false;
35
+ }
36
+ }
37
+
38
+ getBaseUrl() {
39
+ if (!this.config) throw new Error('Streamify not started. Call .start() first');
40
+ return `http://127.0.0.1:${this.config.port}`;
41
+ }
42
+
43
+ detectSource(input) {
44
+ if (!input) return { source: null, id: null, isUrl: false };
45
+
46
+ const patterns = {
47
+ youtube: [
48
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
49
+ /^([a-zA-Z0-9_-]{11})$/
50
+ ],
51
+ spotify: [
52
+ /open\.spotify\.com\/track\/([a-zA-Z0-9]+)/,
53
+ /spotify:track:([a-zA-Z0-9]+)/
54
+ ],
55
+ soundcloud: [
56
+ /soundcloud\.com\/([^\/]+\/[^\/\?]+)/,
57
+ /api\.soundcloud\.com\/tracks\/(\d+)/
58
+ ]
59
+ };
60
+
61
+ for (const [source, regexes] of Object.entries(patterns)) {
62
+ for (const regex of regexes) {
63
+ const match = input.match(regex);
64
+ if (match) {
65
+ return {
66
+ source,
67
+ id: match[1],
68
+ isUrl: input.includes('://') || input.includes('.com')
69
+ };
70
+ }
71
+ }
72
+ }
73
+
74
+ return { source: null, id: null, isUrl: false };
75
+ }
76
+
77
+ async resolve(input, limit = 1) {
78
+ if (!this.running) throw new Error('Streamify not started. Call .start() first');
79
+
80
+ const detected = this.detectSource(input);
81
+
82
+ if (detected.source && detected.isUrl) {
83
+ const track = await this.getInfo(detected.source, detected.id).catch(() => null);
84
+ if (track) {
85
+ return { source: detected.source, tracks: [track], fromUrl: true };
86
+ }
87
+ }
88
+
89
+ if (detected.source === 'youtube' && detected.id && !detected.isUrl) {
90
+ const track = await this.getInfo('youtube', detected.id).catch(() => null);
91
+ if (track) {
92
+ return { source: 'youtube', tracks: [track], fromUrl: true };
93
+ }
94
+ }
95
+
96
+ const results = await this.search('youtube', input, limit);
97
+ return { source: 'youtube', tracks: results.tracks, fromUrl: false, searchTime: results.searchTime };
98
+ }
99
+
100
+ async search(source, query, limit = 10) {
101
+ if (!this.running) throw new Error('Streamify not started. Call .start() first');
102
+
103
+ switch (source) {
104
+ case 'youtube':
105
+ case 'yt':
106
+ return youtube.search(query, limit, this.config);
107
+ case 'spotify':
108
+ case 'sp':
109
+ return spotify.search(query, limit, this.config);
110
+ case 'soundcloud':
111
+ case 'sc':
112
+ return soundcloud.search(query, limit, this.config);
113
+ default:
114
+ throw new Error(`Unknown source: ${source}`);
115
+ }
116
+ }
117
+
118
+ async getInfo(source, id) {
119
+ if (!this.running) throw new Error('Streamify not started. Call .start() first');
120
+
121
+ switch (source) {
122
+ case 'youtube':
123
+ case 'yt':
124
+ return youtube.getInfo(id, this.config);
125
+ case 'spotify':
126
+ case 'sp':
127
+ return spotify.getInfo(id, this.config);
128
+ default:
129
+ throw new Error(`Unknown source: ${source}`);
130
+ }
131
+ }
132
+
133
+ getStreamUrl(source, id, filters = {}) {
134
+ if (!this.config) throw new Error('Streamify not started. Call .start() first');
135
+
136
+ let endpoint;
137
+ switch (source) {
138
+ case 'youtube':
139
+ case 'yt':
140
+ endpoint = `/youtube/stream/${id}`;
141
+ break;
142
+ case 'spotify':
143
+ case 'sp':
144
+ endpoint = `/spotify/stream/${id}`;
145
+ break;
146
+ case 'soundcloud':
147
+ case 'sc':
148
+ endpoint = `/soundcloud/stream/${id}`;
149
+ break;
150
+ default:
151
+ throw new Error(`Unknown source: ${source}`);
152
+ }
153
+
154
+ const params = new URLSearchParams();
155
+ for (const [key, value] of Object.entries(filters)) {
156
+ if (value !== undefined && value !== null) {
157
+ params.append(key, value);
158
+ }
159
+ }
160
+
161
+ const queryString = params.toString();
162
+ return `${this.getBaseUrl()}${endpoint}${queryString ? '?' + queryString : ''}`;
163
+ }
164
+
165
+ async getStream(source, id, filters = {}) {
166
+ const url = this.getStreamUrl(source, id, filters);
167
+ const response = await fetch(url);
168
+ if (!response.ok) {
169
+ throw new Error(`Stream failed: ${response.status}`);
170
+ }
171
+ return response.body;
172
+ }
173
+
174
+ youtube = {
175
+ search: (query, limit = 10) => this.search('youtube', query, limit),
176
+ getInfo: (id) => this.getInfo('youtube', id),
177
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('youtube', id, filters),
178
+ getStream: (id, filters = {}) => this.getStream('youtube', id, filters)
179
+ };
180
+
181
+ spotify = {
182
+ search: (query, limit = 10) => this.search('spotify', query, limit),
183
+ getInfo: (id) => this.getInfo('spotify', id),
184
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('spotify', id, filters),
185
+ getStream: (id, filters = {}) => this.getStream('spotify', id, filters)
186
+ };
187
+
188
+ soundcloud = {
189
+ search: (query, limit = 10) => this.search('soundcloud', query, limit),
190
+ getStreamUrl: (id, filters = {}) => this.getStreamUrl('soundcloud', id, filters),
191
+ getStream: (id, filters = {}) => this.getStream('soundcloud', id, filters)
192
+ };
193
+
194
+ async getActiveStreams() {
195
+ const res = await fetch(`${this.getBaseUrl()}/streams`);
196
+ return res.json();
197
+ }
198
+
199
+ async getStreamInfo(streamId) {
200
+ const res = await fetch(`${this.getBaseUrl()}/streams/${streamId}`);
201
+ if (!res.ok) return null;
202
+ return res.json();
203
+ }
204
+
205
+ async getPosition(streamId) {
206
+ const res = await fetch(`${this.getBaseUrl()}/streams/${streamId}/position`);
207
+ if (!res.ok) return null;
208
+ const data = await res.json();
209
+ return data.position;
210
+ }
211
+
212
+ async applyFilters(source, trackId, currentStreamId, newFilters) {
213
+ const position = await this.getPosition(currentStreamId);
214
+ if (position === null) {
215
+ throw new Error('Stream not found or already ended');
216
+ }
217
+
218
+ const filters = {
219
+ ...newFilters,
220
+ start: Math.floor(position)
221
+ };
222
+
223
+ return {
224
+ url: this.getStreamUrl(source, trackId, filters),
225
+ position,
226
+ filters
227
+ };
228
+ }
229
+ }
230
+
231
+ if (require.main === module) {
232
+ const streamify = new Streamify();
233
+ streamify.start().then(() => {
234
+ console.log(`[STREAMIFY] Server running at ${streamify.getBaseUrl()}`);
235
+ }).catch(err => {
236
+ console.error('[STREAMIFY] Failed to start:', err.message);
237
+ process.exit(1);
238
+ });
239
+ }
240
+
241
+ let Manager = null;
242
+ try {
243
+ Manager = require('./src/discord/Manager');
244
+ } catch (e) {
245
+ }
246
+
247
+ Streamify.Manager = Manager;
248
+
249
+ Streamify.Player = Manager ? require('./src/discord/Player') : null;
250
+ Streamify.Queue = Manager ? require('./src/discord/Queue') : null;
251
+
252
+ module.exports = Streamify;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "streamify-audio",
3
+ "version": "2.0.0",
4
+ "description": "Dual-mode audio library: HTTP streaming proxy + Discord player (Lavalink alternative). Supports YouTube, Spotify, SoundCloud with audio filters.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "bin": {
8
+ "streamify": "./bin/streamify.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "dev": "node index.js --dev"
13
+ },
14
+ "keywords": [
15
+ "discord",
16
+ "music",
17
+ "bot",
18
+ "youtube",
19
+ "spotify",
20
+ "soundcloud",
21
+ "audio",
22
+ "streaming",
23
+ "lavalink",
24
+ "lavalink-alternative",
25
+ "discordjs-voice",
26
+ "yt-dlp",
27
+ "ffmpeg"
28
+ ],
29
+ "author": "Lucas",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "express": "^4.18.2"
33
+ },
34
+ "optionalDependencies": {
35
+ "@discordjs/opus": "^0.9.0",
36
+ "@discordjs/voice": "^0.18.0"
37
+ },
38
+ "peerDependencies": {
39
+ "discord.js": "^14.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "discord.js": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/LucasCzechia/streamify.git"
52
+ },
53
+ "homepage": "https://github.com/LucasCzechia/streamify#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/LucasCzechia/streamify/issues"
56
+ }
57
+ }
package/silence.ogg ADDED
Binary file