streamify-audio 2.2.7 → 2.2.9
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 +2 -2
- package/src/providers/bandcamp.js +1 -0
- package/src/providers/mixcloud.js +1 -0
- package/src/providers/soundcloud.js +18 -4
- package/src/providers/spotify.js +19 -4
- package/src/providers/twitch.js +3 -0
- 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.9",
|
|
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
|
@@ -261,7 +261,7 @@ class Manager extends EventEmitter {
|
|
|
261
261
|
if (!this.config.spotify?.clientId) {
|
|
262
262
|
throw new Error('Spotify credentials not configured');
|
|
263
263
|
}
|
|
264
|
-
result = await spotify.search(query, limit, this.config);
|
|
264
|
+
result = await spotify.search(query, limit, this.config, options);
|
|
265
265
|
break;
|
|
266
266
|
|
|
267
267
|
case 'soundcloud':
|
|
@@ -269,7 +269,7 @@ class Manager extends EventEmitter {
|
|
|
269
269
|
if (!this._isProviderEnabled('soundcloud')) {
|
|
270
270
|
throw new Error('SoundCloud provider is disabled');
|
|
271
271
|
}
|
|
272
|
-
result = await soundcloud.search(query, limit, this.config);
|
|
272
|
+
result = await soundcloud.search(query, limit, this.config, options);
|
|
273
273
|
break;
|
|
274
274
|
|
|
275
275
|
case 'twitch':
|
|
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
|
|
|
33
33
|
title: data.title,
|
|
34
34
|
duration: data.duration || 0,
|
|
35
35
|
author: data.uploader || data.artist,
|
|
36
|
+
authorUrl: data.uploader_url || null,
|
|
36
37
|
thumbnail: data.thumbnail,
|
|
37
38
|
uri: data.webpage_url,
|
|
38
39
|
streamUrl: `/bandcamp/stream/${Buffer.from(url).toString('base64')}`,
|
|
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
|
|
|
33
33
|
title: data.title,
|
|
34
34
|
duration: data.duration || 0,
|
|
35
35
|
author: data.uploader || data.user?.username,
|
|
36
|
+
authorUrl: data.uploader_url || null,
|
|
36
37
|
thumbnail: data.thumbnail,
|
|
37
38
|
uri: data.webpage_url,
|
|
38
39
|
streamUrl: `/mixcloud/stream/${Buffer.from(url).toString('base64')}`,
|
|
@@ -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,16 +34,27 @@ 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,
|
|
39
41
|
author: entry.uploader,
|
|
42
|
+
authorUrl: entry.uploader_url || null,
|
|
40
43
|
thumbnail: entry.thumbnails?.[0]?.url,
|
|
41
44
|
uri: entry.url || entry.webpage_url,
|
|
42
45
|
streamUrl: `/soundcloud/stream/${entry.id}`,
|
|
43
46
|
source: 'soundcloud'
|
|
44
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
|
+
|
|
45
58
|
const elapsed = Date.now() - startTime;
|
|
46
59
|
log.info('SOUNDCLOUD', `Found ${tracks.length} results (${elapsed}ms)`);
|
|
47
60
|
resolve({ tracks, source: 'soundcloud', searchTime: elapsed });
|
|
@@ -82,6 +95,7 @@ async function getInfo(trackId, config) {
|
|
|
82
95
|
title: data.title,
|
|
83
96
|
duration: data.duration,
|
|
84
97
|
author: data.uploader,
|
|
98
|
+
authorUrl: data.uploader_url || null,
|
|
85
99
|
thumbnail: data.thumbnail,
|
|
86
100
|
uri: data.webpage_url,
|
|
87
101
|
streamUrl: `/soundcloud/stream/${data.id}`,
|
package/src/providers/spotify.js
CHANGED
|
@@ -52,16 +52,19 @@ 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(', '),
|
|
67
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
65
68
|
album: track.album.name,
|
|
66
69
|
duration: Math.floor(track.duration_ms / 1000),
|
|
67
70
|
thumbnail: track.album.images?.[0]?.url,
|
|
@@ -70,6 +73,14 @@ async function search(query, limit, config) {
|
|
|
70
73
|
source: 'spotify'
|
|
71
74
|
}));
|
|
72
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
|
+
|
|
73
84
|
const elapsed = Date.now() - startTime;
|
|
74
85
|
log.info('SPOTIFY', `Found ${tracks.length} results (${elapsed}ms)`);
|
|
75
86
|
return { tracks, source: 'spotify', searchTime: elapsed };
|
|
@@ -83,6 +94,7 @@ async function getInfo(trackId, config) {
|
|
|
83
94
|
id: track.id,
|
|
84
95
|
title: track.name,
|
|
85
96
|
author: track.artists.map(a => a.name).join(', '),
|
|
97
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
86
98
|
album: track.album.name,
|
|
87
99
|
duration: Math.floor(track.duration_ms / 1000),
|
|
88
100
|
thumbnail: track.album.images?.[0]?.url,
|
|
@@ -142,6 +154,7 @@ async function getPlaylist(playlistId, config) {
|
|
|
142
154
|
id: item.track.id,
|
|
143
155
|
title: item.track.name,
|
|
144
156
|
author: item.track.artists.map(a => a.name).join(', '),
|
|
157
|
+
authorUrl: item.track.artists[0]?.external_urls?.spotify || null,
|
|
145
158
|
album: item.track.album.name,
|
|
146
159
|
duration: Math.floor(item.track.duration_ms / 1000),
|
|
147
160
|
thumbnail: item.track.album.images?.[0]?.url,
|
|
@@ -171,6 +184,7 @@ async function getAlbum(albumId, config) {
|
|
|
171
184
|
id: track.id,
|
|
172
185
|
title: track.name,
|
|
173
186
|
author: track.artists.map(a => a.name).join(', '),
|
|
187
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
174
188
|
album: data.name,
|
|
175
189
|
duration: Math.floor(track.duration_ms / 1000),
|
|
176
190
|
thumbnail: data.images?.[0]?.url,
|
|
@@ -200,6 +214,7 @@ async function getRecommendations(trackId, limit, config) {
|
|
|
200
214
|
id: track.id,
|
|
201
215
|
title: track.name,
|
|
202
216
|
author: track.artists.map(a => a.name).join(', '),
|
|
217
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
203
218
|
album: track.album.name,
|
|
204
219
|
duration: Math.floor(track.duration_ms / 1000),
|
|
205
220
|
thumbnail: track.album.images?.[0]?.url,
|
package/src/providers/twitch.js
CHANGED
|
@@ -28,11 +28,14 @@ async function getInfo(url, config) {
|
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
30
|
const data = JSON.parse(stdout);
|
|
31
|
+
const authorUrl = data.uploader_url || data.channel_url ||
|
|
32
|
+
(data.channel ? `https://www.twitch.tv/${data.channel}` : null);
|
|
31
33
|
resolve({
|
|
32
34
|
id: data.id,
|
|
33
35
|
title: data.title,
|
|
34
36
|
duration: data.duration || 0,
|
|
35
37
|
author: data.uploader || data.channel,
|
|
38
|
+
authorUrl,
|
|
36
39
|
thumbnail: data.thumbnail,
|
|
37
40
|
uri: data.webpage_url,
|
|
38
41
|
streamUrl: `/twitch/stream/${Buffer.from(url).toString('base64')}`,
|
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) {
|