streamify-audio 2.2.8 → 2.2.10
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/README.md +12 -3
- package/docs/discord/manager.md +8 -0
- package/package.json +1 -1
- package/src/discord/Manager.js +12 -3
- package/src/providers/soundcloud.js +16 -4
- package/src/providers/spotify.js +14 -4
- package/src/providers/youtube.js +16 -6
- package/src/server.js +9 -9
package/README.md
CHANGED
|
@@ -229,12 +229,21 @@ const manager = new Streamify.Manager(client, {
|
|
|
229
229
|
|
|
230
230
|
### Search with Filters
|
|
231
231
|
|
|
232
|
-
Filter for live streams
|
|
232
|
+
Filter for live streams, sort results, or filter by artist.
|
|
233
233
|
|
|
234
234
|
```javascript
|
|
235
235
|
const results = await manager.search('lofi hip hop', {
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
source: 'youtube', // youtube, spotify, soundcloud
|
|
237
|
+
type: 'live', // video, live, playlist
|
|
238
|
+
sort: 'popularity', // relevance, popular, latest
|
|
239
|
+
limit: 10
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Artist search - filters results to only include tracks by that artist
|
|
243
|
+
const artistTracks = await manager.search('Drake', {
|
|
244
|
+
source: 'spotify',
|
|
245
|
+
artist: 'Drake', // filters results to tracks BY this artist
|
|
246
|
+
limit: 5
|
|
238
247
|
});
|
|
239
248
|
```
|
|
240
249
|
|
package/docs/discord/manager.md
CHANGED
|
@@ -76,6 +76,7 @@ Searches for tracks.
|
|
|
76
76
|
- `limit` (number) - Number of results (default: 10)
|
|
77
77
|
- `type` (string) - `video`, `live`, or `all` (YouTube only)
|
|
78
78
|
- `sort` (string) - `relevance`, `popularity`, `date`, or `rating` (YouTube only)
|
|
79
|
+
- `artist` (string) - Filter results to only include tracks by this artist
|
|
79
80
|
|
|
80
81
|
**Example:**
|
|
81
82
|
|
|
@@ -85,6 +86,13 @@ const result = await manager.search('lofi hip hop', {
|
|
|
85
86
|
type: 'live',
|
|
86
87
|
sort: 'popularity'
|
|
87
88
|
});
|
|
89
|
+
|
|
90
|
+
// Artist search - filters results to tracks BY the artist
|
|
91
|
+
const artistTracks = await manager.search('Drake', {
|
|
92
|
+
source: 'spotify',
|
|
93
|
+
artist: 'Drake',
|
|
94
|
+
limit: 5
|
|
95
|
+
});
|
|
88
96
|
```
|
|
89
97
|
|
|
90
98
|
// Result
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "streamify-audio",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.10",
|
|
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
|
@@ -39,12 +39,18 @@ function checkDependencies() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
let managerInitLogged = false;
|
|
43
|
+
|
|
42
44
|
class Manager extends EventEmitter {
|
|
43
45
|
constructor(client, options = {}) {
|
|
44
46
|
super();
|
|
45
47
|
|
|
46
48
|
checkDependencies();
|
|
47
49
|
|
|
50
|
+
if (options.logLevel !== undefined) {
|
|
51
|
+
log.setLevel(options.logLevel);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
this.client = client;
|
|
49
55
|
this.players = new Map();
|
|
50
56
|
|
|
@@ -85,7 +91,10 @@ class Manager extends EventEmitter {
|
|
|
85
91
|
|
|
86
92
|
this._setupVoiceStateListener();
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
if (!managerInitLogged) {
|
|
95
|
+
managerInitLogged = true;
|
|
96
|
+
log.info('MANAGER', 'Streamify Manager initialized');
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
_setupVoiceStateListener() {
|
|
@@ -261,7 +270,7 @@ class Manager extends EventEmitter {
|
|
|
261
270
|
if (!this.config.spotify?.clientId) {
|
|
262
271
|
throw new Error('Spotify credentials not configured');
|
|
263
272
|
}
|
|
264
|
-
result = await spotify.search(query, limit, this.config);
|
|
273
|
+
result = await spotify.search(query, limit, this.config, options);
|
|
265
274
|
break;
|
|
266
275
|
|
|
267
276
|
case 'soundcloud':
|
|
@@ -269,7 +278,7 @@ class Manager extends EventEmitter {
|
|
|
269
278
|
if (!this._isProviderEnabled('soundcloud')) {
|
|
270
279
|
throw new Error('SoundCloud provider is disabled');
|
|
271
280
|
}
|
|
272
|
-
result = await soundcloud.search(query, limit, this.config);
|
|
281
|
+
result = await soundcloud.search(query, limit, this.config, options);
|
|
273
282
|
break;
|
|
274
283
|
|
|
275
284
|
case 'twitch':
|
|
@@ -3,9 +3,11 @@ 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
|
-
|
|
8
|
+
const artistFilter = options.artist ? options.artist.toLowerCase() : null;
|
|
9
|
+
const searchLimit = artistFilter ? Math.min(limit * 3, 25) : limit;
|
|
10
|
+
log.info('SOUNDCLOUD', `Searching: "${query}" (limit: ${limit}${artistFilter ? `, artist: ${options.artist}` : ''})`);
|
|
9
11
|
|
|
10
12
|
return new Promise((resolve, reject) => {
|
|
11
13
|
const args = [
|
|
@@ -13,7 +15,7 @@ async function search(query, limit, config) {
|
|
|
13
15
|
'--flat-playlist',
|
|
14
16
|
'--skip-download',
|
|
15
17
|
'-J',
|
|
16
|
-
`scsearch${
|
|
18
|
+
`scsearch${searchLimit}:${query}`
|
|
17
19
|
];
|
|
18
20
|
|
|
19
21
|
const proc = spawn(config.ytdlpPath, args, {
|
|
@@ -32,7 +34,7 @@ async function search(query, limit, config) {
|
|
|
32
34
|
}
|
|
33
35
|
try {
|
|
34
36
|
const data = JSON.parse(stdout);
|
|
35
|
-
|
|
37
|
+
let tracks = (data.entries || []).map(entry => ({
|
|
36
38
|
id: entry.id,
|
|
37
39
|
title: entry.title,
|
|
38
40
|
duration: entry.duration,
|
|
@@ -43,6 +45,16 @@ async function search(query, limit, config) {
|
|
|
43
45
|
streamUrl: `/soundcloud/stream/${entry.id}`,
|
|
44
46
|
source: 'soundcloud'
|
|
45
47
|
}));
|
|
48
|
+
|
|
49
|
+
if (artistFilter) {
|
|
50
|
+
tracks = tracks.filter(t => {
|
|
51
|
+
const author = (t.author || '').toLowerCase();
|
|
52
|
+
const title = (t.title || '').toLowerCase();
|
|
53
|
+
return author.includes(artistFilter) || title.includes(artistFilter);
|
|
54
|
+
}).slice(0, limit);
|
|
55
|
+
log.info('SOUNDCLOUD', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
const elapsed = Date.now() - startTime;
|
|
47
59
|
log.info('SOUNDCLOUD', `Found ${tracks.length} results (${elapsed}ms)`);
|
|
48
60
|
resolve({ tracks, source: 'soundcloud', searchTime: elapsed });
|
package/src/providers/spotify.js
CHANGED
|
@@ -52,13 +52,15 @@ async function spotifyApi(endpoint, config) {
|
|
|
52
52
|
return response.json();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async function search(query, limit, config) {
|
|
55
|
+
async function search(query, limit, config, options = {}) {
|
|
56
56
|
const startTime = Date.now();
|
|
57
|
-
|
|
57
|
+
const artistFilter = options.artist ? options.artist.toLowerCase() : null;
|
|
58
|
+
const searchLimit = artistFilter ? Math.min(limit * 2, 50) : limit;
|
|
59
|
+
log.info('SPOTIFY', `Searching: "${query}" (limit: ${limit}${artistFilter ? `, artist: ${options.artist}` : ''})`);
|
|
58
60
|
|
|
59
|
-
const data = await spotifyApi(`/search?q=${encodeURIComponent(query)}&type=track&limit=${
|
|
61
|
+
const data = await spotifyApi(`/search?q=${encodeURIComponent(query)}&type=track&limit=${searchLimit}`, config);
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
let tracks = (data.tracks?.items || []).map(track => ({
|
|
62
64
|
id: track.id,
|
|
63
65
|
title: track.name,
|
|
64
66
|
author: track.artists.map(a => a.name).join(', '),
|
|
@@ -71,6 +73,14 @@ async function search(query, limit, config) {
|
|
|
71
73
|
source: 'spotify'
|
|
72
74
|
}));
|
|
73
75
|
|
|
76
|
+
if (artistFilter) {
|
|
77
|
+
tracks = tracks.filter(t => {
|
|
78
|
+
const author = (t.author || '').toLowerCase();
|
|
79
|
+
return author.includes(artistFilter);
|
|
80
|
+
}).slice(0, limit);
|
|
81
|
+
log.info('SPOTIFY', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
const elapsed = Date.now() - startTime;
|
|
75
85
|
log.info('SPOTIFY', `Found ${tracks.length} results (${elapsed}ms)`);
|
|
76
86
|
return { tracks, source: 'spotify', searchTime: elapsed };
|
package/src/providers/youtube.js
CHANGED
|
@@ -5,7 +5,9 @@ const log = require('../utils/logger');
|
|
|
5
5
|
|
|
6
6
|
async function search(query, limit, config, options = {}) {
|
|
7
7
|
const startTime = Date.now();
|
|
8
|
-
|
|
8
|
+
const artistFilter = options.artist ? options.artist.toLowerCase() : null;
|
|
9
|
+
const searchLimit = artistFilter ? Math.min(limit * 3, 25) : limit;
|
|
10
|
+
log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit}, type: ${options.type || 'all'}, sort: ${options.sort || 'relevance'}${artistFilter ? `, artist: ${options.artist}` : ''})`);
|
|
9
11
|
|
|
10
12
|
return new Promise((resolve, reject) => {
|
|
11
13
|
const args = [
|
|
@@ -20,15 +22,13 @@ async function search(query, limit, config, options = {}) {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
let searchQuery = query;
|
|
23
|
-
|
|
24
|
-
// Handle sorting by modifying search query or using filters
|
|
25
|
+
|
|
25
26
|
if (options.sort === 'views' || options.sort === 'popular') {
|
|
26
27
|
searchQuery += ' most viewed';
|
|
27
28
|
} else if (options.sort === 'date' || options.sort === 'latest') {
|
|
28
29
|
searchQuery += ' new';
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
// Handle type filtering
|
|
32
32
|
if (options.type === 'live') {
|
|
33
33
|
args.push('--match-filter', 'live_status = is_live');
|
|
34
34
|
} else if (options.type === 'playlist') {
|
|
@@ -37,7 +37,7 @@ async function search(query, limit, config, options = {}) {
|
|
|
37
37
|
args.push('--match-filter', 'live_status != is_live');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
args.push(`ytsearch${
|
|
40
|
+
args.push(`ytsearch${searchLimit}:${searchQuery}`);
|
|
41
41
|
|
|
42
42
|
const proc = spawn(config.ytdlpPath, args, {
|
|
43
43
|
env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
|
|
@@ -59,7 +59,7 @@ async function search(query, limit, config, options = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
try {
|
|
61
61
|
const data = JSON.parse(stdout);
|
|
62
|
-
|
|
62
|
+
let tracks = (data.entries || []).map(entry => {
|
|
63
63
|
const bestAudio = entry.formats
|
|
64
64
|
?.filter(f => f.vcodec === 'none' && f.acodec !== 'none')
|
|
65
65
|
.sort((a, b) => (b.abr || 0) - (a.abr || 0))[0];
|
|
@@ -82,6 +82,16 @@ async function search(query, limit, config, options = {}) {
|
|
|
82
82
|
_headers: entry.http_headers || data.http_headers || null
|
|
83
83
|
};
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
if (artistFilter) {
|
|
87
|
+
tracks = tracks.filter(t => {
|
|
88
|
+
const author = (t.author || '').toLowerCase();
|
|
89
|
+
const title = (t.title || '').toLowerCase();
|
|
90
|
+
return author.includes(artistFilter) || title.includes(artistFilter);
|
|
91
|
+
}).slice(0, limit);
|
|
92
|
+
log.info('YOUTUBE', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
const elapsed = Date.now() - startTime;
|
|
86
96
|
log.info('YOUTUBE', `Found ${tracks.length} results (${elapsed}ms)`);
|
|
87
97
|
resolve({ tracks, source: 'youtube', searchTime: elapsed });
|
package/src/server.js
CHANGED
|
@@ -98,14 +98,14 @@ class Server {
|
|
|
98
98
|
if (!this._isProviderEnabled('youtube')) {
|
|
99
99
|
return res.status(400).json({ error: 'YouTube provider is disabled' });
|
|
100
100
|
}
|
|
101
|
-
const { q, limit = 10, type, sort } = req.query;
|
|
101
|
+
const { q, limit = 10, type, sort, artist } = req.query;
|
|
102
102
|
if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
|
|
103
103
|
|
|
104
|
-
const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}`;
|
|
104
|
+
const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}:${artist || ''}`;
|
|
105
105
|
const cached = cache.get(cacheKey);
|
|
106
106
|
if (cached) return res.json(cached);
|
|
107
107
|
|
|
108
|
-
const results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
|
|
108
|
+
const results = await youtube.search(q, parseInt(limit), this.config, { type, sort, artist });
|
|
109
109
|
cache.set(cacheKey, results, this.config.cache.searchTTL);
|
|
110
110
|
res.json(results);
|
|
111
111
|
} catch (error) {
|
|
@@ -149,14 +149,14 @@ class Server {
|
|
|
149
149
|
if (!this.config.spotify?.clientId) {
|
|
150
150
|
return res.status(400).json({ error: 'Spotify not configured' });
|
|
151
151
|
}
|
|
152
|
-
const { q, limit = 10 } = req.query;
|
|
152
|
+
const { q, limit = 10, artist } = req.query;
|
|
153
153
|
if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
|
|
154
154
|
|
|
155
|
-
const cacheKey = `sp:search:${q}:${limit}`;
|
|
155
|
+
const cacheKey = `sp:search:${q}:${limit}:${artist || ''}`;
|
|
156
156
|
const cached = cache.get(cacheKey);
|
|
157
157
|
if (cached) return res.json(cached);
|
|
158
158
|
|
|
159
|
-
const results = await spotify.search(q, parseInt(limit), this.config);
|
|
159
|
+
const results = await spotify.search(q, parseInt(limit), this.config, { artist });
|
|
160
160
|
cache.set(cacheKey, results, this.config.cache.searchTTL);
|
|
161
161
|
res.json(results);
|
|
162
162
|
} catch (error) {
|
|
@@ -208,14 +208,14 @@ class Server {
|
|
|
208
208
|
if (!this._isProviderEnabled('soundcloud')) {
|
|
209
209
|
return res.status(400).json({ error: 'SoundCloud provider is disabled' });
|
|
210
210
|
}
|
|
211
|
-
const { q, limit = 10 } = req.query;
|
|
211
|
+
const { q, limit = 10, artist } = req.query;
|
|
212
212
|
if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
|
|
213
213
|
|
|
214
|
-
const cacheKey = `sc:search:${q}:${limit}`;
|
|
214
|
+
const cacheKey = `sc:search:${q}:${limit}:${artist || ''}`;
|
|
215
215
|
const cached = cache.get(cacheKey);
|
|
216
216
|
if (cached) return res.json(cached);
|
|
217
217
|
|
|
218
|
-
const results = await soundcloud.search(q, parseInt(limit), this.config);
|
|
218
|
+
const results = await soundcloud.search(q, parseInt(limit), this.config, { artist });
|
|
219
219
|
cache.set(cacheKey, results, this.config.cache.searchTTL);
|
|
220
220
|
res.json(results);
|
|
221
221
|
} catch (error) {
|